From 22f25f00982b3cbf5daface49dcea6e2a6ec72da Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Fri, 15 May 2026 17:08:08 +1000 Subject: [PATCH] Interface mockup 18 --- src/ifcviewer-full/CMakeLists.txt | 10 +- src/ifcviewer-full/MainWindow.cpp | 143 +++---- src/ifcviewer-full/MainWindow.h | 12 +- src/ifcviewer-full/SessionState.cpp | 62 ++- src/ifcviewer-full/SessionState.h | 23 +- .../models/{Controller.cpp => Commands.cpp} | 398 ++++++++---------- src/ifcviewer-full/modules/models/Commands.h | 67 +++ src/ifcviewer-full/modules/models/Panel.cpp | 121 +++--- src/ifcviewer-full/modules/models/Panel.h | 31 +- .../modules/models/SettingsDialog.cpp | 3 + src/ifcviewer-full/modules/models/Types.h | 7 + src/ifcviewer-full/modules/models/View.cpp | 49 ++- src/ifcviewer-full/modules/models/View.h | 6 +- .../modules/project/Controller.cpp | 52 +-- .../modules/project/Controller.h | 12 - .../modules/properties/View.cpp | 3 + .../modules/viewport/Commands.cpp | 102 +++++ .../Controller.h => viewport/Commands.h} | 59 +-- .../viewport/{Controller.cpp => View.cpp} | 213 +++------- .../modules/viewport/{Controller.h => View.h} | 38 +- 20 files changed, 727 insertions(+), 684 deletions(-) rename src/ifcviewer-full/modules/models/{Controller.cpp => Commands.cpp} (65%) create mode 100644 src/ifcviewer-full/modules/models/Commands.h create mode 100644 src/ifcviewer-full/modules/viewport/Commands.cpp rename src/ifcviewer-full/modules/{models/Controller.h => viewport/Commands.h} (52%) rename src/ifcviewer-full/modules/viewport/{Controller.cpp => View.cpp} (61%) rename src/ifcviewer-full/modules/viewport/{Controller.h => View.h} (73%) diff --git a/src/ifcviewer-full/CMakeLists.txt b/src/ifcviewer-full/CMakeLists.txt index 1ab5b003e6..0ca381ef84 100644 --- a/src/ifcviewer-full/CMakeLists.txt +++ b/src/ifcviewer-full/CMakeLists.txt @@ -58,8 +58,8 @@ set(IFCVIEWER_FULL_FILES ${CMAKE_CURRENT_SOURCE_DIR}/modules/models/SettingsView.cpp ${CMAKE_CURRENT_SOURCE_DIR}/modules/models/SettingsView.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 + ${CMAKE_CURRENT_SOURCE_DIR}/modules/models/Commands.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/modules/models/Commands.h ${CMAKE_CURRENT_SOURCE_DIR}/modules/models/Panel.cpp ${CMAKE_CURRENT_SOURCE_DIR}/modules/models/Panel.h ${CMAKE_CURRENT_SOURCE_DIR}/modules/models/View.cpp @@ -82,8 +82,10 @@ set(IFCVIEWER_FULL_FILES ${CMAKE_CURRENT_SOURCE_DIR}/modules/spatial_hierarchy/View.h ${CMAKE_CURRENT_SOURCE_DIR}/modules/viewport/Panel.cpp ${CMAKE_CURRENT_SOURCE_DIR}/modules/viewport/Panel.h - ${CMAKE_CURRENT_SOURCE_DIR}/modules/viewport/Controller.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/modules/viewport/Controller.h + ${CMAKE_CURRENT_SOURCE_DIR}/modules/viewport/Commands.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/modules/viewport/Commands.h + ${CMAKE_CURRENT_SOURCE_DIR}/modules/viewport/View.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/modules/viewport/View.h ${CMAKE_CURRENT_SOURCE_DIR}/ifcviewer_full_resources.qrc ) diff --git a/src/ifcviewer-full/MainWindow.cpp b/src/ifcviewer-full/MainWindow.cpp index d1874da44d..7f6070b9a7 100644 --- a/src/ifcviewer-full/MainWindow.cpp +++ b/src/ifcviewer-full/MainWindow.cpp @@ -24,13 +24,12 @@ #include "../ifcviewer/Federation.h" #include "../ifcviewer/SceneLoader.h" #include "../ifcviewer/ViewportWindow.h" -#include "ElementRegistry.h" #include "SessionState.h" #include "components/Buttons.h" #include "components/Panel.h" #include "components/Style.h" #include "components/Tabs.h" -#include "modules/models/Controller.h" +#include "modules/models/Commands.h" #include "modules/todo/Panel.h" #include "modules/models/View.h" #include "modules/models/Panel.h" @@ -40,8 +39,9 @@ #include "modules/settings/Dialog.h" #include "modules/spatial_hierarchy/View.h" #include "modules/spatial_hierarchy/Panel.h" -#include "modules/viewport/Controller.h" +#include "modules/viewport/Commands.h" #include "modules/viewport/Panel.h" +#include "modules/viewport/View.h" #include #include @@ -62,23 +62,20 @@ namespace ifcviewerfull::shell { MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) { - federation_ = new Federation(this); - element_registry_ = new ifcviewerfull::ElementRegistry(this); session_state_ = new ifcviewerfull::SessionState(this); - session_state_->bindFederation(federation_); - session_state_->bindElementRegistry(element_registry_); - connect(federation_, &Federation::dirtyChanged, this, [this](bool dirty) { - setWindowModified(dirty); + auto on_mutated = [this]() { + setWindowModified(true); updateWindowTitle(); - }); - connect(session_state_, &ifcviewerfull::SessionState::modelsChanged, - this, &MainWindow::updateWindowTitle); - connect(session_state_, &ifcviewerfull::SessionState::projectOpened, - this, [this](const QString&) { updateWindowTitle(); }); - connect(session_state_, &ifcviewerfull::SessionState::projectSaved, - this, [this](const QString&) { updateWindowTitle(); }); - connect(session_state_, &ifcviewerfull::SessionState::projectReset, - this, &MainWindow::updateWindowTitle); + }; + auto on_clean_state = [this]() { + setWindowModified(false); + updateWindowTitle(); + }; + connect(session_state_, &ifcviewerfull::SessionState::federationChanged, this, on_mutated); + connect(session_state_, &ifcviewerfull::SessionState::modelsChanged, this, on_mutated); + connect(session_state_, &ifcviewerfull::SessionState::projectReset, this, on_clean_state); + connect(session_state_, &ifcviewerfull::SessionState::projectOpened, this, on_clean_state); + connect(session_state_, &ifcviewerfull::SessionState::projectSaved, this, on_clean_state); setupChrome(); setupViewport(); setupPanels(); @@ -100,31 +97,31 @@ void MainWindow::setupChrome() { connect(shortcut, &QShortcut::activated, this, fn); }; bind_shortcut(QKeySequence("Ctrl+Shift+L"), [this]() { - viewport_controller_->toggleDistanceMode(); + modules::viewport::commands::toggleDistance(*viewport_widget_->viewport()); }); bind_shortcut(QKeySequence("Ctrl+Shift+A"), [this]() { - viewport_controller_->toggleAreaMode(); + modules::viewport::commands::toggleArea(*viewport_widget_->viewport()); }); bind_shortcut(QKeySequence("Ctrl+Shift+V"), [this]() { - viewport_controller_->toggleVolumeMode(); + modules::viewport::commands::toggleVolume(*viewport_widget_->viewport()); }); bind_shortcut(QKeySequence(Qt::SHIFT | Qt::Key_F), [this]() { - viewport_controller_->setFlyMode(); + modules::viewport::commands::fly(*session_state_, *viewport_widget_->viewport()); }); bind_shortcut(QKeySequence(Qt::Key_K), [this]() { - viewport_controller_->toggleSectionMode(); + modules::viewport::commands::toggleSection(*session_state_, *viewport_widget_->viewport()); }); bind_shortcut(QKeySequence(Qt::SHIFT | Qt::Key_K), [this]() { - viewport_controller_->clearSectionPlanes(); + modules::viewport::commands::clearSection(*session_state_, *viewport_widget_->viewport()); }); bind_shortcut(QKeySequence(Qt::Key_H), [this]() { - viewport_controller_->hideSelectedElements(); + modules::viewport::commands::hideSelected(*viewport_widget_->viewport()); }); bind_shortcut(QKeySequence(Qt::SHIFT | Qt::Key_H), [this]() { - viewport_controller_->isolateSelectedElements(); + modules::viewport::commands::isolateSelected(*viewport_widget_->viewport()); }); bind_shortcut(QKeySequence(Qt::ALT | Qt::Key_H), [this]() { - viewport_controller_->showAllElements(); + modules::viewport::commands::showAll(*viewport_widget_->viewport()); }); updateWindowTitle(); } @@ -178,7 +175,7 @@ QWidget* MainWindow::buildHomeRibbonPage() { auto* add_model = components::buttons::makeButton("Add Model", ":/icons/cube.svg", this); connect(add_model, &QToolButton::clicked, this, [this]() { - models_controller_->addFiles(); + modules::models::commands::addModel(*session_state_, *this); }); auto* sync_models = components::buttons::makeButton("Sync Models", ":/icons/refresh-double.svg", this); connect(sync_models, &QToolButton::clicked, this, [this]() { @@ -207,11 +204,11 @@ QWidget* MainWindow::buildNavigateRibbonPage() { auto* set_home = components::buttons::makeButton("Set Home", ":/icons/home.svg", this); connect(set_home, &QToolButton::clicked, this, [this]() { - viewport_controller_->setHomeView(); + modules::viewport::commands::setHome(*session_state_, *viewport_widget_->viewport()); }); auto* go_home = components::buttons::makeButton("Go Home", ":/icons/home-alt.svg", this); connect(go_home, &QToolButton::clicked, this, [this]() { - viewport_controller_->goHomeView(); + modules::viewport::commands::goHome(*session_state_, *viewport_widget_->viewport()); }); auto* view_all = components::buttons::makeButton("View All", ":/icons/cube-scan.svg", this); connect(view_all, &QToolButton::clicked, this, [this]() { @@ -219,7 +216,7 @@ QWidget* MainWindow::buildNavigateRibbonPage() { }); auto* view_selected = components::buttons::makeButton("View Selected", ":/icons/cube-scan-solid.svg", this); connect(view_selected, &QToolButton::clicked, this, [this]() { - viewport_controller_->focusSelectedObject(); + modules::viewport::commands::viewSelected(*viewport_widget_->viewport()); }); auto* plan_view = components::buttons::makeButton("Plan", ":/icons/planimetry.svg", this); @@ -248,11 +245,11 @@ QWidget* MainWindow::buildNavigateRibbonPage() { auto* fly_mode = components::buttons::makeButton("Fly", ":/icons/drone.svg", this); connect(fly_mode, &QToolButton::clicked, this, [this]() { - viewport_controller_->setFlyMode(); + modules::viewport::commands::fly(*session_state_, *viewport_widget_->viewport()); }); auto* section_mode = components::buttons::makeButton("Section", ":/icons/cube-cut-with-curve.svg", this); connect(section_mode, &QToolButton::clicked, this, [this]() { - viewport_controller_->toggleSectionMode(); + modules::viewport::commands::toggleSection(*session_state_, *viewport_widget_->viewport()); }); row->addWidget(components::buttons::makeButtonGroup("CAMERA", {set_home, go_home, view_all, view_selected}, this)); @@ -271,32 +268,32 @@ QWidget* MainWindow::buildInspectRibbonPage() { auto* hide_selected = components::buttons::makeButton("Hide", ":/icons/eye-closed.svg", this); connect(hide_selected, &QToolButton::clicked, this, [this]() { - viewport_controller_->hideSelectedElements(); + modules::viewport::commands::hideSelected(*viewport_widget_->viewport()); }); auto* isolate_selected = components::buttons::makeButton("Isolate", ":/icons/eye-solid.svg", this); connect(isolate_selected, &QToolButton::clicked, this, [this]() { - viewport_controller_->isolateSelectedElements(); + modules::viewport::commands::isolateSelected(*viewport_widget_->viewport()); }); auto* show_all = components::buttons::makeButton("Show All", ":/icons/eye.svg", this); connect(show_all, &QToolButton::clicked, this, [this]() { - viewport_controller_->showAllElements(); + modules::viewport::commands::showAll(*viewport_widget_->viewport()); }); auto* invert_selection = components::buttons::makeButton("Invert", ":/icons/intersect.svg", this); connect(invert_selection, &QToolButton::clicked, this, [this]() { - viewport_controller_->invertSelection(); + modules::viewport::commands::invertVisibility(*viewport_widget_->viewport()); }); auto* distance = components::buttons::makeButton("Distance", ":/icons/select-edge3d.svg", this); connect(distance, &QToolButton::clicked, this, [this]() { - viewport_controller_->toggleDistanceMode(); + modules::viewport::commands::toggleDistance(*viewport_widget_->viewport()); }); auto* area = components::buttons::makeButton("Area", ":/icons/select-face3d.svg", this); connect(area, &QToolButton::clicked, this, [this]() { - viewport_controller_->toggleAreaMode(); + modules::viewport::commands::toggleArea(*viewport_widget_->viewport()); }); auto* volume = components::buttons::makeButton("Volume", ":/icons/select-point3d.svg", this); connect(volume, &QToolButton::clicked, this, [this]() { - viewport_controller_->toggleVolumeMode(); + modules::viewport::commands::toggleVolume(*viewport_widget_->viewport()); }); row->addWidget(components::buttons::makeButtonGroup("SELECTION", {hide_selected, isolate_selected, show_all, invert_selection}, this)); @@ -374,12 +371,11 @@ void MainWindow::setupViewport() { } void MainWindow::setupPanels() { - models_panel_ = new modules::models::ModelsPanel(this); + models_panel_ = new modules::models::ModelsPanel( + session_state_, viewport_widget_->viewport(), this); spatial_panel_ = new modules::spatial_hierarchy::SpatialHierarchyPanel(this); properties_panel_ = new modules::properties::PropertiesPanel(this); - models_controller_ = new modules::models::ModelsPanelController( - this, models_panel_, session_state_, viewport_widget_->viewport(), this); models_view_ = new modules::models::ModelsPanelView(models_panel_, session_state_, this); spatial_view_ = new modules::spatial_hierarchy::SpatialHierarchyPanelView(spatial_panel_, session_state_, this); properties_view_ = new modules::properties::PropertiesPanelView(properties_panel_, session_state_, this); @@ -457,44 +453,40 @@ void MainWindow::setupStatus() { } void MainWindow::setupLoader() { - loader_ = new SceneLoader(viewport_widget_->viewport(), this); - element_registry_->bindLoader(loader_); - session_state_->bindLoader(loader_); - models_controller_->bindLoader(loader_); - viewport_controller_ = new modules::viewport::ViewportController( + session_state_->createLoader(viewport_widget_->viewport()); + auto* loader = session_state_->loader(); + viewport_view_ = new modules::viewport::ViewportView( session_state_, viewport_widget_->viewport(), this); project_controller_ = new modules::project::ProjectController( - this, federation_, session_state_, element_registry_, - viewport_widget_->viewport(), models_controller_, viewport_controller_, this); + this, session_state_, viewport_widget_->viewport(), this); - connect(loader_, &SceneLoader::loadStarted, this, - [this](uint32_t /*mid*/, const QString& /*display_name*/) { + // Progress bar — tightly coupled to loader by nature, fine to subscribe direct. + connect(loader, &SceneLoader::loadStarted, this, + [this](uint32_t, const QString&) { status_progress_bar_->setValue(0); status_progress_bar_->setVisible(true); }); - connect(loader_, &SceneLoader::progressChanged, this, - [this](int percent) { - status_progress_bar_->setValue(percent); - }); + connect(loader, &SceneLoader::progressChanged, this, + [this](int percent) { status_progress_bar_->setValue(percent); }); - auto hide_progress = [this]() { + auto hide_progress = [this]() { status_progress_bar_->setVisible(false); }; + connect(loader, &SceneLoader::loadedFromSidecar, this, + [hide_progress](uint32_t, qint64) { hide_progress(); }); + connect(loader, &SceneLoader::loadedFromStream, this, + [this, hide_progress](uint32_t mid, qint64) { + modules::models::commands::writeSidecarForLoadedModel( + *session_state_, *viewport_widget_->viewport(), mid); + hide_progress(); + }); + connect(loader, &SceneLoader::loadCancelled, this, + [hide_progress](uint32_t) { hide_progress(); }); + + // Load errors surface through SessionState as a session-level signal; the + // status text is already set there, we only show the modal here. + connect(session_state_, &ifcviewerfull::SessionState::loadError, this, + [this](const QString& message) { status_progress_bar_->setVisible(false); - }; - connect(loader_, &SceneLoader::loadedFromSidecar, this, - [hide_progress](uint32_t /*mid*/, qint64 /*elapsed_ms*/) { - hide_progress(); - }); - connect(loader_, &SceneLoader::loadedFromStream, this, - [hide_progress](uint32_t /*mid*/, qint64 /*elapsed_ms*/) { - hide_progress(); - }); - connect(loader_, &SceneLoader::loadCancelled, this, - [hide_progress](uint32_t /*mid*/) { - hide_progress(); - }); - connect(loader_, &SceneLoader::loadError, this, - [hide_progress](uint32_t /*mid*/, const QString& /*message*/) { - hide_progress(); + QMessageBox::warning(this, "IfcViewer", message); }); connect(viewport_widget_->viewport(), &ViewportWindow::frameStatsUpdated, this, @@ -518,8 +510,9 @@ void MainWindow::setupLoader() { } void MainWindow::updateWindowTitle() { - const QString project_path = federation_ ? federation_->filePath() : QString(); - if (project_path.isEmpty() && (!federation_ || federation_->models().empty())) { + auto* federation = session_state_->federation(); + const QString project_path = federation->filePath(); + if (project_path.isEmpty() && federation->models().empty()) { setWindowTitle("IfcOpenShell Interface"); } else if (project_path.isEmpty()) { setWindowTitle("untitled[*] - IfcOpenShell Interface"); diff --git a/src/ifcviewer-full/MainWindow.h b/src/ifcviewer-full/MainWindow.h index 3adb7d4a5b..53e4d21b92 100644 --- a/src/ifcviewer-full/MainWindow.h +++ b/src/ifcviewer-full/MainWindow.h @@ -30,12 +30,8 @@ class QDockWidget; class QProgressBar; class QStackedWidget; class QToolButton; -class Federation; -class SceneLoader; -namespace ifcviewerfull { class ElementRegistry; } namespace ifcviewerfull { class SessionState; } namespace ifcviewerfull::components { class TabBar; } -namespace ifcviewerfull::modules::models { class ModelsPanelController; } namespace ifcviewerfull::modules::models { class ModelsPanel; } namespace ifcviewerfull::modules::models { class ModelsPanelView; } namespace ifcviewerfull::modules::project { class ProjectController; } @@ -43,7 +39,7 @@ namespace ifcviewerfull::modules::spatial_hierarchy { class SpatialHierarchyPane namespace ifcviewerfull::modules::spatial_hierarchy { class SpatialHierarchyPanelView; } namespace ifcviewerfull::modules::properties { class PropertiesPanel; } namespace ifcviewerfull::modules::properties { class PropertiesPanelView; } -namespace ifcviewerfull::modules::viewport { class ViewportController; } +namespace ifcviewerfull::modules::viewport { class ViewportView; } namespace ifcviewerfull::modules::viewport { class ViewportPanel; } namespace ifcviewerfull::shell { @@ -68,7 +64,6 @@ private: QToolButton* makePanelToggle(const QString& text, QDockWidget* dock); private: - Federation* federation_ = nullptr; QLabel* status_mode_label_ = nullptr; QLabel* status_selection_label_ = nullptr; QLabel* status_perf_label_ = nullptr; @@ -76,9 +71,7 @@ private: ifcviewerfull::components::TabBar* ribbon_tabs_ = nullptr; QStackedWidget* ribbon_pages_ = nullptr; ifcviewerfull::modules::viewport::ViewportPanel* viewport_widget_ = nullptr; - ifcviewerfull::modules::viewport::ViewportController* viewport_controller_ = nullptr; - SceneLoader* loader_ = nullptr; - ifcviewerfull::ElementRegistry* element_registry_ = nullptr; + ifcviewerfull::modules::viewport::ViewportView* viewport_view_ = nullptr; ifcviewerfull::SessionState* session_state_ = nullptr; ifcviewerfull::modules::models::ModelsPanel* models_panel_ = nullptr; ifcviewerfull::modules::spatial_hierarchy::SpatialHierarchyPanel* spatial_panel_ = nullptr; @@ -90,7 +83,6 @@ private: QDockWidget* audit_panel_ = nullptr; QDockWidget* clash_panel_ = nullptr; QDockWidget* issues_panel_ = nullptr; - ifcviewerfull::modules::models::ModelsPanelController* models_controller_ = nullptr; ifcviewerfull::modules::models::ModelsPanelView* models_view_ = nullptr; ifcviewerfull::modules::project::ProjectController* project_controller_ = nullptr; ifcviewerfull::modules::spatial_hierarchy::SpatialHierarchyPanelView* spatial_view_ = nullptr; diff --git a/src/ifcviewer-full/SessionState.cpp b/src/ifcviewer-full/SessionState.cpp index bcd7c59fac..87636a6cff 100644 --- a/src/ifcviewer-full/SessionState.cpp +++ b/src/ifcviewer-full/SessionState.cpp @@ -20,25 +20,63 @@ #include "SessionState.h" +#include "ElementRegistry.h" #include "../ifcviewer/Federation.h" +#include "../ifcviewer/SceneLoader.h" namespace ifcviewerfull { SessionState::SessionState(QObject* parent) : QObject(parent) + , federation_(new Federation(this)) + , element_registry_(new ElementRegistry(this)) { } -void SessionState::bindFederation(Federation* federation) { - federation_ = federation; -} +void SessionState::createLoader(ViewportWindow* viewport) { + Q_ASSERT(!loader_); + loader_ = new SceneLoader(viewport, this); + element_registry_->bindLoader(loader_); -void SessionState::bindLoader(SceneLoader* loader) { - loader_ = loader; -} + auto format_elapsed = [](qint64 ms) { + return (ms >= 1000) + ? QString::number(ms / 1000.0, 'f', 2) + " s" + : QString::number(ms) + " ms"; + }; -void SessionState::bindElementRegistry(ElementRegistry* element_registry) { - element_registry_ = element_registry; + // Translate low-level loader events into session-level signals + status + // text so views don't need to subscribe to the loader directly. + connect(loader_, &SceneLoader::loadStarted, this, + [this](uint32_t, const QString& display_name) { + setStatusMessage("Loading", display_name); + }); + connect(loader_, &SceneLoader::loadedFromSidecar, this, + [this, format_elapsed](uint32_t mid, qint64 elapsed_ms) { + setStatusMessage("Loaded", + QString("%1 from cache in %2") + .arg(loader_->displayName(mid)) + .arg(format_elapsed(elapsed_ms))); + emit modelGeometryReady(mid); + }); + connect(loader_, &SceneLoader::loadedFromStream, this, + [this, format_elapsed](uint32_t mid, qint64 elapsed_ms) { + setStatusMessage("Loaded", + QString("%1 streamed in %2") + .arg(loader_->displayName(mid)) + .arg(format_elapsed(elapsed_ms))); + emit modelGeometryReady(mid); + }); + connect(loader_, &SceneLoader::loadCancelled, this, [this](uint32_t mid) { + setStatusMessage("Cancelled", loader_->displayName(mid)); + }); + connect(loader_, &SceneLoader::loadError, this, + [this](uint32_t, const QString& message) { + setStatusMessage("Error", message); + emit loadError(message); + }); + connect(loader_, &SceneLoader::allLoadsFinished, this, [this]() { + setStatusMessage("Loaded", QString("%1 model(s)").arg(loader_->modelCount())); + }); } void SessionState::setSelectedObjectId(uint32_t object_id) { @@ -88,14 +126,18 @@ void SessionState::notifyModelsChanged() { emit modelsChanged(); } -void SessionState::notifyFederationStructureChanged() { - emit federationStructureChanged(); +void SessionState::notifyFederationChanged() { + emit federationChanged(); } void SessionState::notifyVisibilityChanged() { emit visibilityChanged(); } +void SessionState::notifyModelGeometryReady(uint32_t model_id) { + emit modelGeometryReady(model_id); +} + void SessionState::notifyProjectOpened(const QString& path) { emit projectOpened(path); } diff --git a/src/ifcviewer-full/SessionState.h b/src/ifcviewer-full/SessionState.h index b62a855fef..de615bcd33 100644 --- a/src/ifcviewer-full/SessionState.h +++ b/src/ifcviewer-full/SessionState.h @@ -27,6 +27,7 @@ class Federation; class SceneLoader; +class ViewportWindow; namespace ifcviewerfull { @@ -38,9 +39,9 @@ class SessionState : public QObject { public: explicit SessionState(QObject* parent = nullptr); - void bindFederation(Federation* federation); - void bindLoader(SceneLoader* loader); - void bindElementRegistry(ElementRegistry* element_registry); + // Owned by SessionState once it can be tied to a viewport. Wires + // loader → element registry signals internally. Call exactly once. + void createLoader(ViewportWindow* viewport); Federation* federation() const { return federation_; } SceneLoader* loader() const { return loader_; } @@ -61,8 +62,9 @@ public: void notifySelectionChanged(); void notifyModelsChanged(); - void notifyFederationStructureChanged(); + void notifyFederationChanged(); void notifyVisibilityChanged(); + void notifyModelGeometryReady(uint32_t model_id); void notifyProjectOpened(const QString& path); void notifyProjectSaved(const QString& path); void notifyProjectReset(); @@ -72,8 +74,19 @@ signals: void projectSaved(const QString& path); void projectReset(); void modelsChanged(); - void federationStructureChanged(); + // Fires whenever a command has mutated the federation (groups, transforms, + // origin, config). Always implies the project is now dirty; callers do not + // emit this on save/load/reset — projectSaved/Opened/Reset cover those. + void federationChanged(); void visibilityChanged(); + // Fires when a model's geometry has been pushed to the viewport. SessionState + // emits this internally in response to SceneLoader signals — callers should + // not need to fire it themselves. + void modelGeometryReady(uint32_t model_id); + // Fires when SceneLoader reports a load failure. SessionState turns the + // raw loader signal into a session-level one so views (e.g. the MessageBox) + // can subscribe without touching the loader directly. + void loadError(const QString& message); void selectionChanged(uint32_t object_id); void statusMessageChanged(const QString& mode, const QString& detail); diff --git a/src/ifcviewer-full/modules/models/Controller.cpp b/src/ifcviewer-full/modules/models/Commands.cpp similarity index 65% rename from src/ifcviewer-full/modules/models/Controller.cpp rename to src/ifcviewer-full/modules/models/Commands.cpp index 6ef9c9214d..9b426484e4 100644 --- a/src/ifcviewer-full/modules/models/Controller.cpp +++ b/src/ifcviewer-full/modules/models/Commands.cpp @@ -18,14 +18,13 @@ * * ********************************************************************************/ -#include "Controller.h" +#include "Commands.h" +#include "AddModelDialog.h" #include "SettingsDialog.h" -#include "Panel.h" #include "../../ElementRegistry.h" #include "../../SessionState.h" -#include "AddModelDialog.h" #include "../../../ifcviewer/Federation.h" #include "../../../ifcviewer/HeadlessSidecarBuilder.h" #include "../../../ifcviewer/LodBuilder.h" @@ -42,6 +41,8 @@ #include #include #include +#include +#include #include #include #include @@ -56,125 +57,144 @@ #include -namespace ifcviewerfull::modules::models { +namespace ifcviewerfull::modules::models::commands { -ModelsPanelController::ModelsPanelController(QWidget* host, - ModelsPanel* widget, - ifcviewerfull::SessionState* session_state, - ViewportWindow* viewport, - QObject* parent) - : QObject(parent) - , host_(host) - , widget_(widget) - , session_state_(session_state) - , viewport_(viewport) -{ - connect(widget_, &ModelsPanel::visibilityToggleRequested, this, - [this](ItemKind kind, const QString& id) { - Federation* federation = session_state_->federation(); - if (kind == ItemKind::Group) { - if (const Federation::Group* group = federation->findGroupById(id)) { - federation->setGroupVisible(id, !group->visible); - session_state_->notifyVisibilityChanged(); - session_state_->setStatusMessage("Models", group->visible ? "Group hidden" : "Group shown"); - } - } else { - if (const Federation::Model* model = federation->findById(id)) { - federation->setModelVisible(id, !model->visible); - session_state_->notifyVisibilityChanged(); - session_state_->setStatusMessage("Models", model->visible ? "Model hidden" : "Model shown"); - } - } - }); - connect(widget_, &ModelsPanel::addGroupRequested, this, - [this](const QString& parent_group_id, const QString& name) { - session_state_->federation()->addGroup(name, parent_group_id); - session_state_->notifyFederationStructureChanged(); - session_state_->setStatusMessage("Models", "Group added"); - }); - connect(widget_, &ModelsPanel::renameGroupRequested, this, - [this](const QString& id, const QString& name) { - session_state_->federation()->setGroupName(id, name); - session_state_->notifyFederationStructureChanged(); - session_state_->setStatusMessage("Models", "Group renamed"); - }); - connect(widget_, &ModelsPanel::moveGroupRequested, this, - [this](const QString& id, const QString& parent_group_id) { - session_state_->federation()->setGroupParent(id, parent_group_id); - session_state_->notifyFederationStructureChanged(); - session_state_->setStatusMessage("Models", parent_group_id.isEmpty() ? "Group moved to root" - : "Group moved"); - }); - connect(widget_, &ModelsPanel::moveModelsRequested, this, - [this](const QStringList& ids, const QString& parent_group_id) { - for (const auto& id : ids) { - session_state_->federation()->setModelGroup(id, parent_group_id); - } - session_state_->notifyFederationStructureChanged(); - session_state_->setStatusMessage("Models", parent_group_id.isEmpty() ? "Model(s) moved to root" - : "Model(s) moved"); - }); - connect(widget_, &ModelsPanel::removeGroupRequested, this, - [this](const QString& id) { - session_state_->federation()->removeGroup(id); - session_state_->notifyFederationStructureChanged(); - session_state_->setStatusMessage("Models", "Group removed"); - }); - connect(widget_, &ModelsPanel::removeModelRequested, this, - [this](const QString& id) { - removeLoadedModel(id); - session_state_->setStatusMessage("Models", "Model removed"); - }); - connect(widget_, &components::Panel::settingsRequested, this, [this]() { - openSettings(); - }); +namespace { + +QString formatElapsed(qint64 ms) { + return (ms >= 1000) + ? QString::number(ms / 1000.0, 'f', 2) + " s" + : QString::number(ms) + " ms"; } -void ModelsPanelController::bindLoader(SceneLoader* loader) { - connect(loader, &SceneLoader::loadStarted, this, - [this](uint32_t /*mid*/, const QString& display_name) { - session_state_->setStatusMessage("Loading", display_name); - }); - connect(loader, &SceneLoader::loadedFromSidecar, this, - [this, loader](uint32_t mid, qint64 elapsed_ms) { - session_state_->setStatusMessage( - "Loaded", - QString("%1 from cache in %2") - .arg(loader->displayName(mid)) - .arg(formatElapsed(elapsed_ms))); - }); - connect(loader, &SceneLoader::loadedFromStream, this, - [this, loader](uint32_t mid, qint64 elapsed_ms) { - writeSidecarForModel(loader, mid); - session_state_->setStatusMessage( - "Loaded", - QString("%1 streamed in %2") - .arg(loader->displayName(mid)) - .arg(formatElapsed(elapsed_ms))); - }); - connect(loader, &SceneLoader::loadCancelled, this, - [this, loader](uint32_t mid) { - session_state_->setStatusMessage("Cancelled", loader->displayName(mid)); - }); - connect(loader, &SceneLoader::loadError, this, - [this, host = host_](uint32_t /*mid*/, const QString& message) { - session_state_->setStatusMessage("Error", message); - QMessageBox::warning(host, "IfcViewer", message); - }); - connect(loader, &SceneLoader::allLoadsFinished, this, - [this, loader]() { - session_state_->setStatusMessage("Loaded", QString("%1 model(s)").arg(loader->modelCount())); - }); +} // namespace + +void toggleVisibility(SessionState& s, ItemKind kind, const QString& id) { + Federation* fed = s.federation(); + if (kind == ItemKind::Group) { + const Federation::Group* group = fed->findGroupById(id); + if (!group) return; + fed->setGroupVisible(id, !group->visible); + s.notifyVisibilityChanged(); + s.setStatusMessage("Models", group->visible ? "Group hidden" : "Group shown"); + } else { + const Federation::Model* model = fed->findById(id); + if (!model) return; + fed->setModelVisible(id, !model->visible); + s.notifyVisibilityChanged(); + s.setStatusMessage("Models", model->visible ? "Model hidden" : "Model shown"); + } } -void ModelsPanelController::addFiles() { - modules::models::AddModelDialog dialog(host_); +void addGroup(SessionState& s, QWidget& host, const QString& parent_group_id) { + bool ok = false; + const QString name = QInputDialog::getText( + &host, "New Group", "Group name:", QLineEdit::Normal, "Group", &ok); + if (!ok) return; + const QString trimmed = name.trimmed(); + if (trimmed.isEmpty()) return; + + s.federation()->addGroup(trimmed, parent_group_id); + s.notifyFederationChanged(); + s.setStatusMessage("Models", "Group added"); +} + +void renameGroup(SessionState& s, QWidget& host, const QString& group_id) { + const Federation::Group* group = s.federation()->findGroupById(group_id); + if (!group) return; + + bool ok = false; + const QString name = QInputDialog::getText( + &host, "Rename Group", "Group name:", QLineEdit::Normal, group->display_name, &ok); + if (!ok) return; + const QString trimmed = name.trimmed(); + if (trimmed.isEmpty()) return; + + s.federation()->setGroupName(group_id, trimmed); + s.notifyFederationChanged(); + s.setStatusMessage("Models", "Group renamed"); +} + +void moveGroup(SessionState& s, const QString& id, const QString& parent_group_id) { + s.federation()->setGroupParent(id, parent_group_id); + s.notifyFederationChanged(); + s.setStatusMessage("Models", parent_group_id.isEmpty() ? "Group moved to root" : "Group moved"); +} + +void moveModels(SessionState& s, const QStringList& ids, const QString& parent_group_id) { + for (const auto& id : ids) { + s.federation()->setModelGroup(id, parent_group_id); + } + s.notifyFederationChanged(); + s.setStatusMessage("Models", parent_group_id.isEmpty() ? "Model(s) moved to root" : "Model(s) moved"); +} + +void removeGroup(SessionState& s, QWidget& host, const QString& group_id) { + const Federation::Group* group = s.federation()->findGroupById(group_id); + if (!group) return; + + const auto choice = QMessageBox::question( + &host, "Remove Group", + QString("Remove group '%1'? Models inside it will move to the parent.").arg(group->display_name), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + if (choice != QMessageBox::Yes) return; + + s.federation()->removeGroup(group_id); + s.notifyFederationChanged(); + s.setStatusMessage("Models", "Group removed"); +} + +void removeModel(SessionState& s, ViewportWindow& vp, QWidget& host, const QString& fed_id) { + const Federation::Model* model = s.federation()->findById(fed_id); + const QString label = model ? model->display_name : fed_id; + const auto choice = QMessageBox::question( + &host, "Remove Model", + QString("Remove model '%1' from the federation?").arg(label), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + if (choice != QMessageBox::Yes) return; + + const uint32_t mid = s.modelIdForFedId(fed_id); + if (mid == 0) { + s.federation()->removeModel(fed_id); + s.notifyFederationChanged(); + s.setStatusMessage("Models", "Model removed"); + return; + } + if (s.loader()->isLoadingModel(mid)) return; + + vp.setSelectedObjectId(0); + s.setSelectedObjectId(0); + s.federation()->removeModel(fed_id); + vp.removeModel(mid); + s.loader()->removeModel(mid); + s.elementRegistry()->removeModel(mid); + s.removeModelMappingByFedId(fed_id); + s.notifySelectionChanged(); + s.notifyModelsChanged(); + s.setStatusMessage("Models", "Model removed"); +} + +namespace detail { + +void loadModels(SessionState& s, const QStringList& paths, const QStringList& fed_ids) { + if (paths.isEmpty()) return; + + const auto ids = s.loader()->addFiles(paths); + for (int i = 0; i < paths.size() && i < static_cast(ids.size()) && i < fed_ids.size(); ++i) { + s.setModelMapping(fed_ids[i], ids[i]); + } +} + +} // namespace detail + +void addModel(SessionState& s, QWidget& host) { + AddModelDialog dialog(&host); if (dialog.exec() != QDialog::Accepted) return; QStringList paths; switch (dialog.selectedMode()) { - case modules::models::SourceMode::IfcFile: { - QFileDialog file_dialog(host_, "Add IFC Files"); + case SourceMode::IfcFile: { + QFileDialog file_dialog(&host, "Add IFC Files"); file_dialog.setFileMode(QFileDialog::ExistingFiles); file_dialog.setNameFilter("IFC Files (*.ifc);;All Files (*)"); file_dialog.setOption(QFileDialog::DontUseNativeDialog, true); @@ -183,8 +203,8 @@ void ModelsPanelController::addFiles() { } break; } - case modules::models::SourceMode::IfcDatabase: { - QFileDialog database_dialog(host_, "Add IFC Databases"); + case SourceMode::IfcDatabase: { + QFileDialog database_dialog(&host, "Add IFC Databases"); database_dialog.setFileMode(QFileDialog::Directory); database_dialog.setOption(QFileDialog::ShowDirsOnly, true); database_dialog.setOption(QFileDialog::DontResolveSymlinks, true); @@ -200,8 +220,8 @@ void ModelsPanelController::addFiles() { } break; } - case modules::models::SourceMode::GeometryOnly: { - QFileDialog file_dialog(host_, "Add Geometry Only"); + case SourceMode::GeometryOnly: { + QFileDialog file_dialog(&host, "Add Geometry Only"); file_dialog.setFileMode(QFileDialog::ExistingFiles); file_dialog.setNameFilter("IFC Viewer Cache (*.ifcview);;All Files (*)"); file_dialog.setOption(QFileDialog::DontUseNativeDialog, true); @@ -210,21 +230,30 @@ void ModelsPanelController::addFiles() { } break; } - case modules::models::SourceMode::ConvertToDatabase: - convertIfcToDatabase(); + case SourceMode::ConvertToDatabase: + convertIfcToDatabase(s, host); return; - case modules::models::SourceMode::ExportGeometryDatabase: - exportGeometryDatabase(); + case SourceMode::ExportGeometryDatabase: + exportGeometryDatabase(s, host); return; - case modules::models::SourceMode::None: + case SourceMode::None: return; } - addFiles(paths); + QStringList accepted_paths; + QStringList accepted_fed_ids; + for (const auto& path : paths) { + const QString fed_id = s.federation()->addModel(path); + if (fed_id.isEmpty()) continue; + accepted_paths << path; + accepted_fed_ids << fed_id; + } + detail::loadModels(s, accepted_paths, accepted_fed_ids); + s.notifyModelsChanged(); } -void ModelsPanelController::convertIfcToDatabase() { - QFileDialog input_dialog(host_, "Select IFC File to Convert"); +void convertIfcToDatabase(SessionState& s, QWidget& host) { + QFileDialog input_dialog(&host, "Select IFC File to Convert"); input_dialog.setFileMode(QFileDialog::ExistingFile); input_dialog.setNameFilter("IFC Files (*.ifc);;All Files (*)"); input_dialog.setOption(QFileDialog::DontUseNativeDialog, true); @@ -236,7 +265,7 @@ void ModelsPanelController::convertIfcToDatabase() { const QFileInfo input_info(input_path); const QString default_output = input_info.absoluteDir().filePath(input_info.completeBaseName() + ".rdb"); - QFileDialog output_dialog(host_, "Save IFC Database As"); + QFileDialog output_dialog(&host, "Save IFC Database As"); output_dialog.setAcceptMode(QFileDialog::AcceptSave); output_dialog.setFileMode(QFileDialog::AnyFile); output_dialog.setNameFilter("IFC Database (*.rdb);;All Files (*)"); @@ -255,17 +284,13 @@ void ModelsPanelController::convertIfcToDatabase() { const QFileInfo output_info(output_path); if (output_info.exists()) { const QString message = QString("'%1' already exists. Overwrite?").arg(output_info.fileName()); - if (QMessageBox::question(host_, "Convert IFC to Database", message, + if (QMessageBox::question(&host, "Convert IFC to Database", message, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) != QMessageBox::Yes) { return; } } - runIfcToDatabaseConversion(input_path, output_path); -} - -void ModelsPanelController::runIfcToDatabaseConversion(const QString& input_path, const QString& output_path) { - auto* progress = new QProgressDialog(host_); + auto* progress = new QProgressDialog(&host); progress->setWindowTitle("Convert IFC to Database"); progress->setLabelText(QString("Converting %1 to %2…") .arg(QFileInfo(input_path).fileName(), @@ -278,7 +303,7 @@ void ModelsPanelController::runIfcToDatabaseConversion(const QString& input_path progress->setAutoReset(false); progress->show(); - session_state_->setStatusMessage("Converting", + s.setStatusMessage("Converting", QString("%1 → %2").arg(QFileInfo(input_path).fileName(), QFileInfo(output_path).fileName())); auto timer = std::make_shared(); @@ -312,8 +337,8 @@ void ModelsPanelController::runIfcToDatabaseConversion(const QString& input_path } }); - connect(thread, &QThread::finished, this, - [this, thread, progress, timer, error_message, input_path, output_path]() { + QObject::connect(thread, &QThread::finished, &host, + [&s, host_ptr = &host, thread, progress, timer, error_message, input_path, output_path]() { const qint64 elapsed = timer->elapsed(); progress->close(); @@ -321,27 +346,27 @@ void ModelsPanelController::runIfcToDatabaseConversion(const QString& input_path thread->deleteLater(); if (!error_message->isEmpty()) { - session_state_->setStatusMessage("Error", *error_message); - QMessageBox::warning(host_, "Convert IFC to Database", + s.setStatusMessage("Error", *error_message); + QMessageBox::warning(host_ptr, "Convert IFC to Database", QString("Conversion failed:\n%1").arg(*error_message)); return; } - session_state_->setStatusMessage( + s.setStatusMessage( "Converted", QString("%1 → %2 in %3") .arg(QFileInfo(input_path).fileName(), QFileInfo(output_path).fileName(), formatElapsed(elapsed))); - QMessageBox::information(host_, "Convert IFC to Database", + QMessageBox::information(host_ptr, "Convert IFC to Database", QString("Database written to:\n%1").arg(output_path)); }); thread->start(); } -void ModelsPanelController::exportGeometryDatabase() { - QFileDialog input_dialog(host_, "Select IFC File to Export"); +void exportGeometryDatabase(SessionState& s, QWidget& host) { + QFileDialog input_dialog(&host, "Select IFC File to Export"); input_dialog.setFileMode(QFileDialog::ExistingFile); input_dialog.setNameFilter("IFC Files (*.ifc);;All Files (*)"); input_dialog.setOption(QFileDialog::DontUseNativeDialog, true); @@ -353,7 +378,7 @@ void ModelsPanelController::exportGeometryDatabase() { const QFileInfo input_info(input_path); const QString default_output = input_info.absoluteDir().filePath(input_info.completeBaseName() + ".rdbview"); - QFileDialog output_dialog(host_, "Save Geometry Database As"); + QFileDialog output_dialog(&host, "Save Geometry Database As"); output_dialog.setAcceptMode(QFileDialog::AcceptSave); output_dialog.setFileMode(QFileDialog::AnyFile); output_dialog.setNameFilter("Geometry Database (*.rdbview);;All Files (*)"); @@ -368,11 +393,7 @@ void ModelsPanelController::exportGeometryDatabase() { output_path += ".rdbview"; } - runGeometryDatabaseExport(input_path, output_path); -} - -void ModelsPanelController::runGeometryDatabaseExport(const QString& input_path, const QString& output_path) { - auto* progress = new QProgressDialog(host_); + auto* progress = new QProgressDialog(&host); progress->setWindowTitle("Export Geometry Database"); progress->setLabelText(QString("Exporting %1 to %2…") .arg(QFileInfo(input_path).fileName(), @@ -385,7 +406,7 @@ void ModelsPanelController::runGeometryDatabaseExport(const QString& input_path, progress->setAutoReset(false); progress->show(); - session_state_->setStatusMessage("Exporting", + s.setStatusMessage("Exporting", QString("%1 → %2").arg(QFileInfo(input_path).fileName(), QFileInfo(output_path).fileName())); auto timer = std::make_shared(); @@ -407,7 +428,6 @@ void ModelsPanelController::runGeometryDatabaseExport(const QString& input_path, const QString tmp_rdb_dir = QDir(tmp_root).filePath("model.rdb"); try { - // Step 1: lossy RDB with IfcRepresentationItem stripped. ifcopenshell::serializers::document_serializer_context context; context.file = nullptr; context.input_filename = input_path.toStdString(); @@ -429,7 +449,6 @@ void ModelsPanelController::runGeometryDatabaseExport(const QString& input_path, serializer->finalize(); serializer.reset(); - // Step 2: .ifcview sidecar via the headless builder. HeadlessSidecarBuilder builder; if (!builder.build(input_path, tmp_anchor)) { throw ifcopenshell::exception( @@ -440,7 +459,6 @@ void ModelsPanelController::runGeometryDatabaseExport(const QString& input_path, ("Sidecar build reported success but " + tmp_sidecar + " is missing").toStdString()); } - // Step 3: zip the sidecar + RDB directory into the .rdbview. // Write to a sibling `.tmp` then rename so a partial file never // appears at the destination (matters for cloud-sync folders). const QString tmp_zip = output_path + ".tmp"; @@ -498,8 +516,8 @@ void ModelsPanelController::runGeometryDatabaseExport(const QString& input_path, QDir(tmp_root).removeRecursively(); }); - connect(thread, &QThread::finished, this, - [this, thread, progress, timer, error_message, input_path, output_path]() { + QObject::connect(thread, &QThread::finished, &host, + [&s, host_ptr = &host, thread, progress, timer, error_message, input_path, output_path]() { const qint64 elapsed = timer->elapsed(); progress->close(); @@ -507,83 +525,36 @@ void ModelsPanelController::runGeometryDatabaseExport(const QString& input_path, thread->deleteLater(); if (!error_message->isEmpty()) { - session_state_->setStatusMessage("Error", *error_message); - QMessageBox::warning(host_, "Export Geometry Database", + s.setStatusMessage("Error", *error_message); + QMessageBox::warning(host_ptr, "Export Geometry Database", QString("Export failed:\n%1").arg(*error_message)); return; } - session_state_->setStatusMessage( + s.setStatusMessage( "Exported", QString("%1 → %2 in %3") .arg(QFileInfo(input_path).fileName(), QFileInfo(output_path).fileName(), formatElapsed(elapsed))); - QMessageBox::information(host_, "Export Geometry Database", + QMessageBox::information(host_ptr, "Export Geometry Database", QString("Geometry database written to:\n%1").arg(output_path)); }); thread->start(); } -void ModelsPanelController::addFiles(const QStringList& paths) { - QStringList accepted_paths; - QStringList accepted_fed_ids; - for (const auto& path : paths) { - const QString fed_id = session_state_->federation()->addModel(path); - if (fed_id.isEmpty()) continue; - accepted_paths << path; - accepted_fed_ids << fed_id; - } - - loadModels(accepted_paths, accepted_fed_ids); -} - -void ModelsPanelController::loadModels(const QStringList& paths, const QStringList& fed_ids) { - if (paths.isEmpty()) return; - - const auto ids = session_state_->loader()->addFiles(paths); - for (int i = 0; i < paths.size() && i < static_cast(ids.size()) && i < fed_ids.size(); ++i) { - session_state_->setModelMapping(fed_ids[i], ids[i]); - } - session_state_->notifyModelsChanged(); -} - -void ModelsPanelController::removeLoadedModel(const QString& fed_id) { - const uint32_t mid = session_state_->modelIdForFedId(fed_id); - if (mid == 0) { - session_state_->federation()->removeModel(fed_id); - return; - } - if (session_state_->loader()->isLoadingModel(mid)) return; - - viewport_->setSelectedObjectId(0); - session_state_->setSelectedObjectId(0); - session_state_->federation()->removeModel(fed_id); - viewport_->removeModel(mid); - session_state_->loader()->removeModel(mid); - session_state_->elementRegistry()->removeModel(mid); - session_state_->removeModelMappingByFedId(fed_id); - session_state_->notifySelectionChanged(); - session_state_->notifyModelsChanged(); -} - -void ModelsPanelController::openSettings() { - SettingsDialog dialog(session_state_, host_); +void openSettings(SessionState& s, QWidget& host) { + SettingsDialog dialog(&s, &host); dialog.exec(); } -QString ModelsPanelController::formatElapsed(qint64 ms) const { - return (ms >= 1000) - ? QString::number(ms / 1000.0, 'f', 2) + " s" - : QString::number(ms) + " ms"; -} - -void ModelsPanelController::writeSidecarForModel(SceneLoader* loader, uint32_t mid) const { - if (!loader || !viewport_ || !session_state_) return; +void writeSidecarForLoadedModel(SessionState& s, ViewportWindow& vp, uint32_t mid) { + SceneLoader* loader = s.loader(); + if (!loader) return; SidecarData sidecar_data; - if (!viewport_->snapshotModel(mid, sidecar_data)) return; + if (!vp.snapshotModel(mid, sidecar_data)) return; if (const ModelGeoref* georef = loader->modelGeoref(mid)) { sidecar_data.has_coordinate_operation = georef->has_coordinate_operation ? 1 : 0; @@ -593,8 +564,7 @@ void ModelsPanelController::writeSidecarForModel(SceneLoader* loader, uint32_t m sidecar_data.map_unit_to_meters = georef->units.map_unit_to_meters; } - auto* element_registry = session_state_->elementRegistry(); - if (element_registry) { + if (auto* element_registry = s.elementRegistry()) { for (const auto& info : element_registry->basicElementInfoForModel(mid)) { PackedElementInfo packed; packed.object_id = info.object_id; @@ -631,7 +601,7 @@ void ModelsPanelController::writeSidecarForModel(SceneLoader* loader, uint32_t m lod_stats.meshes_with_lod1, lod_stats.meshes_total, lod_stats.tris_lod0_for_lod1, lod_stats.tris_lod1); - viewport_->applyLodExtension(mid, sidecar_data); + vp.applyLodExtension(mid, sidecar_data); QElapsedTimer sidecar_timer; sidecar_timer.start(); @@ -639,4 +609,4 @@ void ModelsPanelController::writeSidecarForModel(SceneLoader* loader, uint32_t m qDebug(" Sidecar write: %lld ms (%s)", sidecar_timer.elapsed(), ok ? "ok" : "FAILED"); } -} // namespace ifcviewerfull::modules::models +} // namespace ifcviewerfull::modules::models::commands diff --git a/src/ifcviewer-full/modules/models/Commands.h b/src/ifcviewer-full/modules/models/Commands.h new file mode 100644 index 0000000000..2bb923f493 --- /dev/null +++ b/src/ifcviewer-full/modules/models/Commands.h @@ -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 . * + * * + ********************************************************************************/ + +#ifndef IFCINTERFACE_MODULES_MODELS_COMMANDS_H +#define IFCINTERFACE_MODULES_MODELS_COMMANDS_H + +#include "Types.h" + +#include +#include +#include + +class QWidget; +class ViewportWindow; +namespace ifcviewerfull { class SessionState; } + +namespace ifcviewerfull::modules::models::commands { + +// User-facing commands. Each one is responsible for emitting any notify() +// signals exactly once, at the end of its execution. +void toggleVisibility(SessionState& s, ItemKind kind, const QString& id); +void addGroup(SessionState& s, QWidget& host, const QString& parent_group_id); +void renameGroup(SessionState& s, QWidget& host, const QString& group_id); +void moveGroup(SessionState& s, const QString& id, const QString& parent_group_id); +void moveModels(SessionState& s, const QStringList& ids, const QString& parent_group_id); +void removeGroup(SessionState& s, QWidget& host, const QString& group_id); +void removeModel(SessionState& s, ViewportWindow& vp, QWidget& host, const QString& fed_id); +void addModel(SessionState& s, QWidget& host); +void convertIfcToDatabase(SessionState& s, QWidget& host); +void exportGeometryDatabase(SessionState& s, QWidget& host); +void openSettings(SessionState& s, QWidget& host); + +// Snapshots the in-memory geometry + element registry for a freshly streamed +// model and persists it as a sidecar next to the source IFC. Called after +// SceneLoader::loadedFromStream so subsequent loads can skip the stream phase. +void writeSidecarForLoadedModel(SessionState& s, ViewportWindow& vp, uint32_t mid); + +// Internal building blocks shared by commands here and by ProjectController. +// These NEVER call notify*() — the caller is responsible for emitting once +// at the end of its execution. +namespace detail { + +// Queues already-federated models on the loader and maps their fed-ids to mids. +void loadModels(SessionState& s, const QStringList& paths, const QStringList& fed_ids); + +} // namespace detail + +} // namespace ifcviewerfull::modules::models::commands + +#endif diff --git a/src/ifcviewer-full/modules/models/Panel.cpp b/src/ifcviewer-full/modules/models/Panel.cpp index 61a3982abc..a866d97eec 100644 --- a/src/ifcviewer-full/modules/models/Panel.cpp +++ b/src/ifcviewer-full/modules/models/Panel.cpp @@ -20,6 +20,8 @@ #include "Panel.h" +#include "Commands.h" + #include "../../ViewerSettings.h" #include "../../components/Section.h" #include "../../components/SvgIcon.h" @@ -30,8 +32,6 @@ #include #include #include -#include -#include #include #include #include @@ -205,12 +205,6 @@ private: } }; -QString promptGroupName(QWidget* parent, const QString& title, const QString& label, const QString& value) { - bool ok = false; - const QString name = QInputDialog::getText(parent, title, label, QLineEdit::Normal, value, &ok); - return ok ? name.trimmed() : QString(); -} - QString itemId(QTreeWidgetItem* item) { return item ? item->data(0, Qt::UserRole + 1).toString() : QString(); } @@ -229,8 +223,12 @@ QList selectedItemsOfKind(QTreeWidget* tree, ItemKind kind) { } // namespace -ModelsPanel::ModelsPanel(QWidget* parent) +ModelsPanel::ModelsPanel(ifcviewerfull::SessionState* session_state, + ViewportWindow* viewport, + QWidget* parent) : components::Panel("Models", nullptr, parent, true) + , session_state_(session_state) + , viewport_(viewport) { auto* section = new components::Section("", components::SectionHeaderMode::Hidden, this); section->setBodyExpanding(true); @@ -254,19 +252,17 @@ ModelsPanel::ModelsPanel(QWidget* parent) tree_->header()->resizeSection(1, 28); tree_->header()->hide(); tree->on_model_drop = [this](const QStringList& ids, const QString& target_group_id) { - emit moveModelsRequested(ids, target_group_id); + commands::moveModels(*session_state_, ids, target_group_id); }; tree->on_group_drop = [this](const QString& id, const QString& target_group_id) { - emit moveGroupRequested(id, target_group_id); + commands::moveGroup(*session_state_, id, target_group_id); }; section->addBodyWidget(tree_); addBodyWidget(section); connect(tree_, &QTreeWidget::itemClicked, this, [this](QTreeWidgetItem* item, int column) { if (!item || column != 1) return; - emit visibilityToggleRequested( - static_cast(item->data(0, Qt::UserRole).toInt()), - item->data(0, Qt::UserRole + 1).toString()); + commands::toggleVisibility(*session_state_, itemKind(item), itemId(item)); }); connect(tree_, &QTreeWidget::customContextMenuRequested, this, [this](const QPoint& pos) { @@ -276,10 +272,7 @@ ModelsPanel::ModelsPanel(QWidget* parent) QAction* add_group_action = menu.addAction( components::icons::makeSvgIcon(":/icons/folder-plus.svg"), "Add Group"); connect(add_group_action, &QAction::triggered, this, [this]() { - const QString name = promptGroupName(this, "New Group", "Group name:", "Group"); - if (!name.isEmpty()) { - emit addGroupRequested(QString(), name); - } + commands::addGroup(*session_state_, *this, QString()); }); menu.exec(tree_->viewport()->mapToGlobal(pos)); return; @@ -291,59 +284,41 @@ ModelsPanel::ModelsPanel(QWidget* parent) QAction* toggle_visibility_action = menu.addAction( components::icons::makeSvgIcon(":/icons/eye.svg"), "Toggle Visibility"); connect(toggle_visibility_action, &QAction::triggered, this, [this, kind, id]() { - emit visibilityToggleRequested(kind, id); + commands::toggleVisibility(*session_state_, kind, id); }); if (kind == ItemKind::Group) { QAction* add_group_action = menu.addAction( components::icons::makeSvgIcon(":/icons/folder-plus.svg"), "New Subgroup"); connect(add_group_action, &QAction::triggered, this, [this, id]() { - const QString name = promptGroupName(this, "New Group", "Group name:", "Group"); - if (!name.isEmpty()) { - emit addGroupRequested(id, name); - } + commands::addGroup(*session_state_, *this, id); }); QAction* rename_group_action = menu.addAction( components::icons::makeSvgIcon(":/icons/folder.svg"), "Rename Group"); - connect(rename_group_action, &QAction::triggered, this, [this, item, id]() { - const QString name = promptGroupName( - this, "Rename Group", "Group name:", item->text(0)); - if (!name.isEmpty()) emit renameGroupRequested(id, name); + connect(rename_group_action, &QAction::triggered, this, [this, id]() { + commands::renameGroup(*session_state_, *this, id); }); + QMenu* move_menu = menu.addMenu("Move to Parent"); QAction* move_root_action = move_menu->addAction("(Root)"); connect(move_root_action, &QAction::triggered, this, [this, id]() { - emit moveGroupRequested(id, QString()); + commands::moveGroup(*session_state_, id, QString()); }); move_menu->addSeparator(); - QList stack; - for (int i = 0; i < tree_->topLevelItemCount(); ++i) { - stack.push_back(tree_->topLevelItem(i)); - } - while (!stack.isEmpty()) { - QTreeWidgetItem* candidate = stack.takeFirst(); - if (candidate != item && itemKind(candidate) == ItemKind::Group) { - bool would_cycle = false; - for (QTreeWidgetItem* cur = candidate; cur != nullptr; cur = cur->parent()) { - if (cur == item) { - would_cycle = true; - break; - } - } - auto* action = move_menu->addAction(candidate->text(0)); - action->setEnabled(!would_cycle && candidate != item->parent()); - connect(action, &QAction::triggered, this, [this, id, candidate]() { - emit moveGroupRequested(id, itemId(candidate)); - }); - } - for (int i = 0; i < candidate->childCount(); ++i) { - stack.push_back(candidate->child(i)); - } + const QList targets = + group_list_provider_ ? group_list_provider_(id) : QList{}; + for (const auto& target : targets) { + QAction* action = move_menu->addAction(target.display_name); + const QString target_id = target.id; + connect(action, &QAction::triggered, this, [this, id, target_id]() { + commands::moveGroup(*session_state_, id, target_id); + }); } + QAction* remove_group_action = menu.addAction( components::icons::makeSvgIcon(":/icons/folder-minus.svg"), "Remove Group"); connect(remove_group_action, &QAction::triggered, this, [this, id]() { - emit removeGroupRequested(id); + commands::removeGroup(*session_state_, *this, id); }); } else { QString parent_group_id; @@ -356,10 +331,7 @@ ModelsPanel::ModelsPanel(QWidget* parent) QAction* add_group_action = menu.addAction( components::icons::makeSvgIcon(":/icons/folder-plus.svg"), "New Group"); connect(add_group_action, &QAction::triggered, this, [this, parent_group_id]() { - const QString name = promptGroupName(this, "New Group", "Group name:", "Group"); - if (!name.isEmpty()) { - emit addGroupRequested(parent_group_id, name); - } + commands::addGroup(*session_state_, *this, parent_group_id); }); QStringList selected_model_ids; @@ -376,34 +348,31 @@ ModelsPanel::ModelsPanel(QWidget* parent) QMenu* move_menu = menu.addMenu("Move to Group"); QAction* move_root_action = move_menu->addAction("(Root)"); connect(move_root_action, &QAction::triggered, this, [this, selected_model_ids]() { - emit moveModelsRequested(selected_model_ids, QString()); + commands::moveModels(*session_state_, selected_model_ids, QString()); }); move_menu->addSeparator(); - QList stack; - for (int i = 0; i < tree_->topLevelItemCount(); ++i) { - stack.push_back(tree_->topLevelItem(i)); - } - while (!stack.isEmpty()) { - QTreeWidgetItem* candidate = stack.takeFirst(); - if (itemKind(candidate) == ItemKind::Group) { - const QString group_id = itemId(candidate); - QAction* action = move_menu->addAction(candidate->text(0)); - connect(action, &QAction::triggered, this, [this, selected_model_ids, group_id]() { - emit moveModelsRequested(selected_model_ids, group_id); - }); - } - for (int i = 0; i < candidate->childCount(); ++i) { - stack.push_back(candidate->child(i)); - } + const QList targets = + group_list_provider_ ? group_list_provider_(QString()) : QList{}; + for (const auto& target : targets) { + QAction* action = move_menu->addAction(target.display_name); + const QString target_id = target.id; + connect(action, &QAction::triggered, this, [this, selected_model_ids, target_id]() { + commands::moveModels(*session_state_, selected_model_ids, target_id); + }); } + QAction* remove_model_action = menu.addAction( components::icons::makeSvgIcon(":/icons/minus-square.svg"), "Remove Model"); connect(remove_model_action, &QAction::triggered, this, [this, id]() { - emit removeModelRequested(id); + commands::removeModel(*session_state_, *viewport_, *this, id); }); } menu.exec(tree_->viewport()->mapToGlobal(pos)); }); + + connect(this, &components::Panel::settingsRequested, this, [this]() { + commands::openSettings(*session_state_, *this); + }); } void ModelsPanel::setNodes(const QList& nodes) { @@ -418,6 +387,10 @@ void ModelsPanel::setNodes(const QList& nodes) { tree_->expandAll(); } +void ModelsPanel::setGroupListProvider(GroupListProvider provider) { + group_list_provider_ = std::move(provider); +} + void ModelsPanel::addNode(QTreeWidgetItem* parent, const TreeNode& node) { auto* item = new QTreeWidgetItem(parent, {node.name, ""}); item->setData(0, Qt::UserRole, static_cast(node.kind)); diff --git a/src/ifcviewer-full/modules/models/Panel.h b/src/ifcviewer-full/modules/models/Panel.h index 88f4b2e068..4f7c934f58 100644 --- a/src/ifcviewer-full/modules/models/Panel.h +++ b/src/ifcviewer-full/modules/models/Panel.h @@ -25,31 +25,42 @@ #include "../../components/Panel.h" +#include + class QTreeWidget; class QTreeWidgetItem; +class ViewportWindow; +namespace ifcviewerfull { class SessionState; } namespace ifcviewerfull::modules::models { +// The widget for the Models dock. Owns no domain state; its click handlers +// call commands directly. The View tells it what to render (setNodes) and +// supplies derived data for the right-click menu (setGroupListProvider). class ModelsPanel : public components::Panel { Q_OBJECT public: - explicit ModelsPanel(QWidget* parent = nullptr); + // Returns the groups a move operation may target. If exclude_subtree_root + // is non-empty, that group + its descendants are excluded so a group can't + // be moved into its own subtree. For model moves the panel passes an empty + // string and gets every group back. + using GroupListProvider = + std::function(const QString& exclude_subtree_root)>; + + explicit ModelsPanel(ifcviewerfull::SessionState* session_state, + ViewportWindow* viewport, + 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 renameGroupRequested(const QString& id, const QString& name); - void moveGroupRequested(const QString& id, const QString& parent_group_id); - void moveModelsRequested(const QStringList& ids, const QString& parent_group_id); - void removeGroupRequested(const QString& id); - void removeModelRequested(const QString& id); + void setGroupListProvider(GroupListProvider provider); private: void addNode(QTreeWidgetItem* parent, const TreeNode& node); + ifcviewerfull::SessionState* session_state_ = nullptr; + ViewportWindow* viewport_ = nullptr; QTreeWidget* tree_ = nullptr; + GroupListProvider group_list_provider_; }; } // namespace ifcviewerfull::modules::models diff --git a/src/ifcviewer-full/modules/models/SettingsDialog.cpp b/src/ifcviewer-full/modules/models/SettingsDialog.cpp index dce1191dd7..b5a0dc7780 100644 --- a/src/ifcviewer-full/modules/models/SettingsDialog.cpp +++ b/src/ifcviewer-full/modules/models/SettingsDialog.cpp @@ -475,6 +475,9 @@ void SettingsDialog::onAccepted() { xf.pivot = parseVector3(row.pivot->text()); federation_->setModelTransformation(row.fed_id, xf); } + if (session_state_) { + session_state_->notifyFederationChanged(); + } } accept(); } diff --git a/src/ifcviewer-full/modules/models/Types.h b/src/ifcviewer-full/modules/models/Types.h index bd313ba737..b8599b8467 100644 --- a/src/ifcviewer-full/modules/models/Types.h +++ b/src/ifcviewer-full/modules/models/Types.h @@ -39,6 +39,13 @@ struct TreeNode { QList children; }; +// One entry in a "move to..." menu. Computed by the View from federation state +// and passed into the Panel so menu construction has no domain knowledge. +struct GroupOption { + QString id; + QString display_name; +}; + struct SelectedModelGeorefState { QString georef_present; QString coordinate_operation_type; diff --git a/src/ifcviewer-full/modules/models/View.cpp b/src/ifcviewer-full/modules/models/View.cpp index d16e5f97e2..e7924e2e85 100644 --- a/src/ifcviewer-full/modules/models/View.cpp +++ b/src/ifcviewer-full/modules/models/View.cpp @@ -54,31 +54,44 @@ TreeNode makeGroupNode(const Federation* federation, const Federation::Group* gr return node; } +// Walks the federation's group tree, appending every group except those under +// exclude_subtree_root (used to prevent a group from being moved into itself). +void collectGroupsRecursive(const Federation::Group* group, + const QString& exclude_subtree_root, + QList& out) { + if (group->id == exclude_subtree_root) return; + out.append({group->id, group->display_name}); + for (const auto& child : group->children) { + collectGroupsRecursive(child.get(), exclude_subtree_root, out); + } +} + } // namespace ModelsPanelView::ModelsPanelView(ModelsPanel* widget, ifcviewerfull::SessionState* session_state, QObject* parent) - : QObject(parent), widget_(widget), session_state_(session_state) + : QObject(parent) + , widget_(widget) + , session_state_(session_state) { - connect(session_state_, &ifcviewerfull::SessionState::modelsChanged, - this, [this]() { reload(); }); - connect(session_state_, &ifcviewerfull::SessionState::federationStructureChanged, - this, [this]() { reload(); }); - connect(session_state_, &ifcviewerfull::SessionState::visibilityChanged, - this, [this]() { reload(); }); - connect(session_state_, &ifcviewerfull::SessionState::projectReset, - this, [this]() { reload(); }); - connect(session_state_, &ifcviewerfull::SessionState::projectOpened, - this, [this](const QString&) { reload(); }); + connect(session_state_, &SessionState::modelsChanged, this, &ModelsPanelView::refresh); + connect(session_state_, &SessionState::federationChanged, this, &ModelsPanelView::refresh); + connect(session_state_, &SessionState::visibilityChanged, this, &ModelsPanelView::refresh); + connect(session_state_, &SessionState::projectReset, this, &ModelsPanelView::refresh); + connect(session_state_, &SessionState::projectOpened, this, [this](const QString&) { refresh(); }); connect(&ifcviewerfull::ViewerSettings::instance(), &ifcviewerfull::ViewerSettings::themeChanged, - this, [this]() { reload(); }); + this, &ModelsPanelView::refresh); - reload(); + widget_->setGroupListProvider([this](const QString& exclude_subtree_root) { + return groupListForMove(exclude_subtree_root); + }); + + refresh(); } -void ModelsPanelView::reload() { +void ModelsPanelView::refresh() { Federation* federation = session_state_->federation(); QList nodes; for (const auto& root_group : federation->rootGroups()) { @@ -97,4 +110,12 @@ void ModelsPanelView::reload() { widget_->setNodes(nodes); } +QList ModelsPanelView::groupListForMove(const QString& exclude_subtree_root) const { + QList out; + for (const auto& root : session_state_->federation()->rootGroups()) { + collectGroupsRecursive(root.get(), exclude_subtree_root, out); + } + return out; +} + } // namespace ifcviewerfull::modules::models diff --git a/src/ifcviewer-full/modules/models/View.h b/src/ifcviewer-full/modules/models/View.h index 6ca38a7231..4cf3375b13 100644 --- a/src/ifcviewer-full/modules/models/View.h +++ b/src/ifcviewer-full/modules/models/View.h @@ -31,6 +31,9 @@ namespace ifcviewerfull::modules::models { class ModelsPanel; +// Subscribes to SessionState and re-derives panel state (tree nodes, valid +// move targets) from the federation. The panel calls commands directly for +// input, so this object is purely state→view; it has no command knowledge. class ModelsPanelView : public QObject { Q_OBJECT public: @@ -39,7 +42,8 @@ public: QObject* parent = nullptr); private: - void reload(); + void refresh(); + QList groupListForMove(const QString& exclude_subtree_root) const; ModelsPanel* widget_ = nullptr; ifcviewerfull::SessionState* session_state_ = nullptr; diff --git a/src/ifcviewer-full/modules/project/Controller.cpp b/src/ifcviewer-full/modules/project/Controller.cpp index 9dbfa9b4ce..98a0a2e799 100644 --- a/src/ifcviewer-full/modules/project/Controller.cpp +++ b/src/ifcviewer-full/modules/project/Controller.cpp @@ -22,8 +22,7 @@ #include "../../ElementRegistry.h" #include "../../SessionState.h" -#include "../models/Controller.h" -#include "../viewport/Controller.h" +#include "../models/Commands.h" #include "../../../ifcviewer/Federation.h" #include "../../../ifcviewer/SceneLoader.h" #include "../../../ifcviewer/ViewportWindow.h" @@ -35,21 +34,13 @@ namespace ifcviewerfull::modules::project { ProjectController::ProjectController(QWidget* host, - Federation* federation, ifcviewerfull::SessionState* session_state, - ifcviewerfull::ElementRegistry* element_registry, ViewportWindow* viewport, - ifcviewerfull::modules::models::ModelsPanelController* models_controller, - ifcviewerfull::modules::viewport::ViewportController* viewport_controller, QObject* parent) : QObject(parent) , host_(host) - , federation_(federation) , session_state_(session_state) - , element_registry_(element_registry) , viewport_(viewport) - , models_controller_(models_controller) - , viewport_controller_(viewport_controller) { } @@ -64,8 +55,7 @@ bool ProjectController::newProject() { if (!confirmDiscardIfDirty()) return false; clearScene(); - federation_->clear(); - viewport_controller_->applyFederatedFalseOrigin(); + session_state_->federation()->clear(); session_state_->setStatusMessage("Project", "Untitled"); session_state_->notifyProjectReset(); return true; @@ -95,7 +85,7 @@ bool ProjectController::openProject(const QString& path) { QStringList warnings; QString err; - if (!federation_->load(path, &warnings, &err)) { + if (!session_state_->federation()->load(path, &warnings, &err)) { QMessageBox::warning(host_, "Open Project", QString("Could not open project:\n%1").arg(err)); return false; @@ -105,7 +95,7 @@ bool ProjectController::openProject(const QString& path) { QStringList paths; QStringList fed_ids; - for (const auto& model : federation_->models()) { + for (const auto& model : session_state_->federation()->models()) { if (model.source_kind != "local") continue; if (!QFileInfo::exists(model.source_path)) { warnings << QString("Source not found, kept in project: %1").arg(model.source_path); @@ -114,17 +104,16 @@ bool ProjectController::openProject(const QString& path) { paths << model.source_path; fed_ids << model.id; } - models_controller_->loadModels(paths, fed_ids); + ifcviewerfull::modules::models::commands::detail::loadModels(*session_state_, paths, fed_ids); if (!warnings.isEmpty()) { QMessageBox::warning(host_, "Open Project", "Project opened with warnings:\n\n" + warnings.join("\n")); } - federation_->markClean(); - viewport_controller_->applyFederatedFalseOrigin(); - if (federation_->hasHomeView()) { - const auto& hv = federation_->homeView(); + session_state_->federation()->markClean(); + if (session_state_->federation()->hasHomeView()) { + const auto& hv = session_state_->federation()->homeView(); viewport_->setCamera( hv.target.x(), hv.target.y(), hv.target.z(), hv.distance, hv.yaw, hv.pitch); } @@ -134,21 +123,21 @@ bool ProjectController::openProject(const QString& path) { } bool ProjectController::saveProject() { - if (federation_->filePath().isEmpty()) return saveProjectAs(); + if (session_state_->federation()->filePath().isEmpty()) return saveProjectAs(); QString err; - if (!federation_->save(federation_->filePath(), &err)) { + if (!session_state_->federation()->save(session_state_->federation()->filePath(), &err)) { QMessageBox::warning(host_, "Save Project", QString("Could not save project:\n%1").arg(err)); return false; } - session_state_->setStatusMessage("Project", QFileInfo(federation_->filePath()).fileName()); - session_state_->notifyProjectSaved(federation_->filePath()); + session_state_->setStatusMessage("Project", QFileInfo(session_state_->federation()->filePath()).fileName()); + session_state_->notifyProjectSaved(session_state_->federation()->filePath()); return true; } bool ProjectController::saveProjectAs() { - QString suggested = federation_->filePath(); + QString suggested = session_state_->federation()->filePath(); if (suggested.isEmpty()) suggested = "project.ifcfed"; QFileDialog file_dialog(host_, "Save Project As", suggested); @@ -166,7 +155,7 @@ bool ProjectController::saveProjectAs() { bool ProjectController::saveProjectAs(const QString& path) { QString err; - if (!federation_->save(path, &err)) { + if (!session_state_->federation()->save(path, &err)) { QMessageBox::warning(host_, "Save Project", QString("Could not save project:\n%1").arg(err)); return false; @@ -177,23 +166,20 @@ bool ProjectController::saveProjectAs(const QString& path) { } void ProjectController::clearScene() { + // Helper for newProject / openProject. Does not emit any notifies; the + // caller emits projectReset / projectOpened once at the end of its flow. viewport_->setSelectedObjectId(0); session_state_->setSelectedObjectId(0); - session_state_->notifySelectionChanged(); - - const auto model_ids = session_state_->modelIds(); - for (uint32_t mid : model_ids) { + for (uint32_t mid : session_state_->modelIds()) { viewport_->removeModel(mid); session_state_->loader()->removeModel(mid); } - session_state_->clearModelMappings(); - element_registry_->clear(); - session_state_->notifyModelsChanged(); + session_state_->elementRegistry()->clear(); } bool ProjectController::confirmDiscardIfDirty() { - if (!federation_->isDirty()) return true; + if (!session_state_->federation()->isDirty()) return true; const auto result = QMessageBox::question( host_, "Unsaved Project", "The current project has unsaved changes. Save before continuing?", diff --git a/src/ifcviewer-full/modules/project/Controller.h b/src/ifcviewer-full/modules/project/Controller.h index 81f56f4609..5382d68a84 100644 --- a/src/ifcviewer-full/modules/project/Controller.h +++ b/src/ifcviewer-full/modules/project/Controller.h @@ -24,12 +24,8 @@ #include class QWidget; -class Federation; class ViewportWindow; -namespace ifcviewerfull { class ElementRegistry; } namespace ifcviewerfull { class SessionState; } -namespace ifcviewerfull::modules::models { class ModelsPanelController; } -namespace ifcviewerfull::modules::viewport { class ViewportController; } namespace ifcviewerfull::modules::project { @@ -38,12 +34,8 @@ class ProjectController : public QObject { public: explicit ProjectController(QWidget* host, - Federation* federation, ifcviewerfull::SessionState* session_state, - ifcviewerfull::ElementRegistry* element_registry, ViewportWindow* viewport, - ifcviewerfull::modules::models::ModelsPanelController* models_controller, - ifcviewerfull::modules::viewport::ViewportController* viewport_controller, QObject* parent = nullptr); bool newProject(); @@ -58,12 +50,8 @@ private: bool confirmDiscardIfDirty(); QWidget* host_ = nullptr; - Federation* federation_ = nullptr; ifcviewerfull::SessionState* session_state_ = nullptr; - ifcviewerfull::ElementRegistry* element_registry_ = nullptr; ViewportWindow* viewport_ = nullptr; - ifcviewerfull::modules::models::ModelsPanelController* models_controller_ = nullptr; - ifcviewerfull::modules::viewport::ViewportController* viewport_controller_ = nullptr; }; } // namespace ifcviewerfull::modules::project diff --git a/src/ifcviewer-full/modules/properties/View.cpp b/src/ifcviewer-full/modules/properties/View.cpp index a98b49bbde..6dc34355d4 100644 --- a/src/ifcviewer-full/modules/properties/View.cpp +++ b/src/ifcviewer-full/modules/properties/View.cpp @@ -38,6 +38,9 @@ PropertiesPanelView::PropertiesPanelView(PropertiesPanel* widget, connect(session_state_, &ifcviewerfull::SessionState::projectReset, this, [this]() { refresh(0); }); + connect(session_state_, &ifcviewerfull::SessionState::projectOpened, this, [this](const QString&) { + refresh(0); + }); refresh(0); } diff --git a/src/ifcviewer-full/modules/viewport/Commands.cpp b/src/ifcviewer-full/modules/viewport/Commands.cpp new file mode 100644 index 0000000000..4bbcdd4dd8 --- /dev/null +++ b/src/ifcviewer-full/modules/viewport/Commands.cpp @@ -0,0 +1,102 @@ +// This file was generated with the assistance of an AI coding tool. +/******************************************************************************** + * * + * This file is part of IfcOpenShell. * + * * + * IfcOpenShell is free software: you can redistribute it and/or modify * + * it under the terms of the Lesser GNU General Public License as published by * + * the Free Software Foundation, either version 3.0 of the License, or * + * (at your option) any later version. * + * * + * IfcOpenShell is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * Lesser GNU General Public License for more details. * + * * + * You should have received a copy of the Lesser GNU General Public License * + * along with this program. If not, see . * + * * + ********************************************************************************/ + +#include "Commands.h" + +#include "../../SessionState.h" +#include "../../../ifcviewer/Federation.h" +#include "../../../ifcviewer/ViewportWindow.h" + +namespace ifcviewerfull::modules::viewport::commands { + +void setHome(SessionState& session, ViewportWindow& vp) { + auto camera = vp.cameraState(); + Federation::HomeView home_view; + home_view.target = camera.target; + home_view.distance = camera.distance; + home_view.yaw = camera.yaw; + home_view.pitch = camera.pitch; + session.federation()->setHomeView(home_view); + session.setStatusMessage("Camera", "Home view updated"); +} + +void goHome(SessionState& session, ViewportWindow& vp) { + Federation* federation = session.federation(); + if (!federation->hasHomeView()) { + session.setStatusMessage("Camera", "No home view set for this project"); + return; + } + const auto& home_view = federation->homeView(); + vp.setCamera( + home_view.target.x(), home_view.target.y(), home_view.target.z(), + home_view.distance, home_view.yaw, home_view.pitch); + session.setStatusMessage("Camera", "Home view restored"); +} + +void viewSelected(ViewportWindow& vp) { + vp.focusOnSelectedObject(); +} + +void fly(SessionState& session, ViewportWindow& vp) { + vp.requestActivate(); + vp.enterFpsMode(); + session.setStatusMessage("Mode", "Fly mode active"); +} + +void toggleSection(SessionState& session, ViewportWindow& vp) { + vp.toggleSectionTool(); + session.setStatusMessage("Section", + vp.sectionToolActive() ? "Section tool active" : "Section tool off"); +} + +void clearSection(SessionState& session, ViewportWindow& vp) { + vp.clearSectionPlanes(); + session.setStatusMessage("Section", "Section planes cleared"); +} + +void toggleDistance(ViewportWindow& vp) { + vp.toggleLengthTool(); +} + +void toggleArea(ViewportWindow& vp) { + vp.toggleAreaTool(); +} + +void toggleVolume(ViewportWindow& vp) { + vp.toggleVolumeTool(); +} + +void hideSelected(ViewportWindow& vp) { + vp.hideSelectedElements(); +} + +void isolateSelected(ViewportWindow& vp) { + vp.isolateSelectedElements(); +} + +void showAll(ViewportWindow& vp) { + vp.showAllElements(); +} + +void invertVisibility(ViewportWindow& vp) { + vp.invertElementVisibility(); +} + +} // namespace ifcviewerfull::modules::viewport::commands diff --git a/src/ifcviewer-full/modules/models/Controller.h b/src/ifcviewer-full/modules/viewport/Commands.h similarity index 52% rename from src/ifcviewer-full/modules/models/Controller.h rename to src/ifcviewer-full/modules/viewport/Commands.h index 6177342437..16b7a14321 100644 --- a/src/ifcviewer-full/modules/models/Controller.h +++ b/src/ifcviewer-full/modules/viewport/Commands.h @@ -18,54 +18,31 @@ * * ********************************************************************************/ -#ifndef IFCINTERFACE_PANELS_MODELSPANELCONTROLLER_H -#define IFCINTERFACE_PANELS_MODELSPANELCONTROLLER_H +#ifndef IFCINTERFACE_MODULES_VIEWPORT_COMMANDS_H +#define IFCINTERFACE_MODULES_VIEWPORT_COMMANDS_H -#include "Types.h" - -#include -#include - -class QWidget; -namespace ifcviewerfull { class SessionState; } class ViewportWindow; -class SceneLoader; +namespace ifcviewerfull { class SessionState; } -namespace ifcviewerfull::modules::models { +namespace ifcviewerfull::modules::viewport::commands { -class ModelsPanel; +void setHome(SessionState& session, ViewportWindow& vp); +void goHome(SessionState& session, ViewportWindow& vp); +void viewSelected(ViewportWindow& vp); -class ModelsPanelController : public QObject { - Q_OBJECT +void fly(SessionState& session, ViewportWindow& vp); +void toggleSection(SessionState& session, ViewportWindow& vp); +void clearSection(SessionState& session, ViewportWindow& vp); -public: - explicit ModelsPanelController(QWidget* host, - ModelsPanel* widget, - ifcviewerfull::SessionState* session_state, - ViewportWindow* viewport, - QObject* parent = nullptr); +void toggleDistance(ViewportWindow& vp); +void toggleArea(ViewportWindow& vp); +void toggleVolume(ViewportWindow& vp); - void bindLoader(SceneLoader* loader); - void addFiles(); - void addFiles(const QStringList& paths); - void loadModels(const QStringList& paths, const QStringList& fed_ids); - void removeLoadedModel(const QString& fed_id); - void openSettings(); - void convertIfcToDatabase(); - void exportGeometryDatabase(); +void hideSelected(ViewportWindow& vp); +void isolateSelected(ViewportWindow& vp); +void showAll(ViewportWindow& vp); +void invertVisibility(ViewportWindow& vp); -private: - QString formatElapsed(qint64 ms) const; - void writeSidecarForModel(SceneLoader* loader, uint32_t mid) const; - void runIfcToDatabaseConversion(const QString& input_path, const QString& output_path); - void runGeometryDatabaseExport(const QString& input_path, const QString& output_path); - - QWidget* host_ = nullptr; - ModelsPanel* widget_ = nullptr; - ifcviewerfull::SessionState* session_state_ = nullptr; - ViewportWindow* viewport_ = nullptr; -}; - -} // namespace ifcviewerfull::modules::models +} // namespace ifcviewerfull::modules::viewport::commands #endif diff --git a/src/ifcviewer-full/modules/viewport/Controller.cpp b/src/ifcviewer-full/modules/viewport/View.cpp similarity index 61% rename from src/ifcviewer-full/modules/viewport/Controller.cpp rename to src/ifcviewer-full/modules/viewport/View.cpp index f7d8a2736c..ffd36c8118 100644 --- a/src/ifcviewer-full/modules/viewport/Controller.cpp +++ b/src/ifcviewer-full/modules/viewport/View.cpp @@ -18,11 +18,10 @@ * * ********************************************************************************/ -#include "Controller.h" +#include "View.h" #include "../../ViewerSettings.h" #include "../../SessionState.h" -#include "../../../ifcviewer/AppSettings.h" #include "../../../ifcviewer/Federation.h" #include "../../../ifcviewer/SceneLoader.h" #include "../../../ifcviewer/ViewportWindow.h" @@ -36,68 +35,30 @@ namespace ifcviewerfull::modules::viewport { -ViewportController::ViewportController(ifcviewerfull::SessionState* session_state, - ViewportWindow* viewport, - QObject* parent) +ViewportView::ViewportView(ifcviewerfull::SessionState* session_state, + ViewportWindow* viewport, + QObject* parent) : QObject(parent) , session_state_(session_state) , viewport_(viewport) , area_measurement_(std::make_unique()) , length_measurement_(std::make_unique()) { - Federation* federation = session_state_->federation(); - SceneLoader* loader = session_state_->loader(); - connect(&ifcviewerfull::ViewerSettings::instance(), - &ifcviewerfull::ViewerSettings::themeChanged, - this, [this]() { - viewport_->setBackgroundColor(QColor(ifcviewerfull::ViewerSettings::instance().color("viewport_background"))); - }); - viewport_->setBackgroundColor(QColor(ifcviewerfull::ViewerSettings::instance().color("viewport_background"))); - connect(federation, &Federation::federatedFalseOriginChanged, - this, &ViewportController::applyFederatedFalseOrigin); - connect(federation, &Federation::configChanged, this, [this]() { - applyFederatedFalseOrigin(); - for (uint32_t mid : session_state_->modelIds()) { - applyModelTransformation(mid); - } - }); - connect(federation, &Federation::modelTransformationChanged, - this, [this](const QString& fed_id) { - const uint32_t mid = session_state_->modelIdForFedId(fed_id); - if (mid != 0) applyModelTransformation(mid); - }); - connect(federation, &Federation::modelVisibilityChanged, - this, [this](const QString& fed_id, bool /*visible*/) { - const uint32_t mid = session_state_->modelIdForFedId(fed_id); - if (mid != 0) applyModelVisibility(mid); - }); - connect(federation, &Federation::modelGroupChanged, - this, [this](const QString& fed_id, const QString& /*group_id*/) { - const uint32_t mid = session_state_->modelIdForFedId(fed_id); - if (mid != 0) applyModelVisibility(mid); - }); - connect(federation, &Federation::groupVisibilityChanged, - this, [this](const QString&, bool /*visible*/) { - for (uint32_t mid : session_state_->modelIds()) { - applyModelVisibility(mid); - } - }); - connect(loader, &SceneLoader::loadedFromSidecar, this, - [this](uint32_t mid, qint64 /*elapsed_ms*/) { - applyCoordinateOperation(mid); - applyModelVisibility(mid); - maybeGuessFederatedFalseOrigin(mid); - }); - connect(loader, &SceneLoader::dataSourceReady, this, - [this](uint32_t mid) { - applyCoordinateOperation(mid); - }); - connect(loader, &SceneLoader::loadedFromStream, this, - [this](uint32_t mid, qint64 /*elapsed_ms*/) { - applyCoordinateOperation(mid); - applyModelVisibility(mid); - maybeGuessFederatedFalseOrigin(mid); + auto& settings = ifcviewerfull::ViewerSettings::instance(); + connect(&settings, &ifcviewerfull::ViewerSettings::themeChanged, this, [this]() { + viewport_->setBackgroundColor( + QColor(ifcviewerfull::ViewerSettings::instance().color("viewport_background"))); }); + viewport_->setBackgroundColor(QColor(settings.color("viewport_background"))); + + connect(session_state_, &SessionState::projectReset, this, &ViewportView::refresh); + connect(session_state_, &SessionState::projectOpened, this, [this](const QString&) { refresh(); }); + connect(session_state_, &SessionState::modelsChanged, this, &ViewportView::refresh); + connect(session_state_, &SessionState::federationChanged, this, &ViewportView::refresh); + connect(session_state_, &SessionState::visibilityChanged, this, &ViewportView::refresh); + connect(session_state_, &SessionState::modelGeometryReady, this, [this](uint32_t) { refresh(); }); + + // Measurement tools — input-driven, share the View's lifetime. connect(viewport_, &ViewportWindow::surfacePickedInTool, this, [this](int x, int y, int modifiers) { const bool alt = (modifiers & Qt::AltModifier) != 0; @@ -148,11 +109,25 @@ ViewportController::ViewportController(ifcviewerfull::SessionState* session_stat connect(viewport_, &ViewportWindow::objectPicked, this, [this](uint32_t) { updateVolumeReadout(); }); + + refresh(); } -ViewportController::~ViewportController() = default; +ViewportView::~ViewportView() = default; -void ViewportController::applyCoordinateOperation(uint32_t mid) { +void ViewportView::refresh() { + Federation* federation = session_state_->federation(); + viewport_->setFederatedFalseOrigin( + composeFederatedFalseOrigin(federation->federatedFalseOrigin(), federation->config())); + + for (uint32_t mid : session_state_->modelIds()) { + applyCoordinateOperation(mid); + applyModelVisibility(mid); + maybeGuessFederatedFalseOrigin(mid); + } +} + +void ViewportView::applyCoordinateOperation(uint32_t mid) { SceneLoader* loader = session_state_->loader(); Eigen::Matrix4d matrix = Eigen::Matrix4d::Identity(); if (const ModelGeoref* georef = loader->modelGeoref(mid)) { @@ -164,7 +139,7 @@ void ViewportController::applyCoordinateOperation(uint32_t mid) { applyModelTransformation(mid); } -void ViewportController::applyModelTransformation(uint32_t mid) { +void ViewportView::applyModelTransformation(uint32_t mid) { Federation* federation = session_state_->federation(); SceneLoader* loader = session_state_->loader(); Eigen::Matrix4d matrix = Eigen::Matrix4d::Identity(); @@ -186,7 +161,7 @@ void ViewportController::applyModelTransformation(uint32_t mid) { viewport_->setModelTransformation(mid, matrix); } -void ViewportController::applyModelVisibility(uint32_t mid) { +void ViewportView::applyModelVisibility(uint32_t mid) { Federation* federation = session_state_->federation(); const QString fed_id = session_state_->fedIdForModelId(mid); if (fed_id.isEmpty()) return; @@ -198,90 +173,27 @@ void ViewportController::applyModelVisibility(uint32_t mid) { } } -void ViewportController::applyFederatedFalseOrigin() { +void ViewportView::maybeGuessFederatedFalseOrigin(uint32_t mid) { Federation* federation = session_state_->federation(); - viewport_->setFederatedFalseOrigin( - composeFederatedFalseOrigin(federation->federatedFalseOrigin(), federation->config())); + SceneLoader* loader = session_state_->loader(); + if (!federation->filePath().isEmpty()) return; + + const FederatedFalseOrigin& current = federation->federatedFalseOrigin(); + const FederatedFalseOrigin defaults; + if (current.xyz != defaults.xyz || current.rz_deg != defaults.rz_deg) return; + + const Eigen::Matrix4d* placement = loader->firstPlacement(mid); + const ModelGeoref* georef = loader->modelGeoref(mid); + if (placement == nullptr || georef == nullptr) return; + + federation->setFederatedFalseOrigin(guessFederatedFalseOrigin( + *placement, *georef, federation->config())); + // The federation mutation above will emit its own signal, but to keep + // views off the Federation bus we re-emit through SessionState. + session_state_->notifyFederationChanged(); } -void ViewportController::setHomeView() { - auto camera = viewport_->cameraState(); - Federation::HomeView home_view; - home_view.target = camera.target; - home_view.distance = camera.distance; - home_view.yaw = camera.yaw; - home_view.pitch = camera.pitch; - session_state_->federation()->setHomeView(home_view); - session_state_->setStatusMessage("Camera", "Home view updated"); -} - -void ViewportController::goHomeView() { - Federation* federation = session_state_->federation(); - if (!federation->hasHomeView()) { - session_state_->setStatusMessage("Camera", "No home view set for this project"); - return; - } - - const auto& home_view = federation->homeView(); - viewport_->setCamera( - home_view.target.x(), home_view.target.y(), home_view.target.z(), - home_view.distance, home_view.yaw, home_view.pitch); - session_state_->setStatusMessage("Camera", "Home view restored"); -} - -void ViewportController::setFlyMode() { - viewport_->requestActivate(); - viewport_->enterFpsMode(); - session_state_->setStatusMessage("Mode", "Fly mode active"); -} - -void ViewportController::toggleSectionMode() { - viewport_->toggleSectionTool(); - if (viewport_->sectionToolActive()) { - session_state_->setStatusMessage("Section", "Section tool active"); - } else { - session_state_->setStatusMessage("Section", "Section tool off"); - } -} - -void ViewportController::clearSectionPlanes() { - viewport_->clearSectionPlanes(); - session_state_->setStatusMessage("Section", "Section planes cleared"); -} - -void ViewportController::toggleDistanceMode() { - viewport_->toggleLengthTool(); -} - -void ViewportController::toggleAreaMode() { - viewport_->toggleAreaTool(); -} - -void ViewportController::toggleVolumeMode() { - viewport_->toggleVolumeTool(); -} - -void ViewportController::focusSelectedObject() { - viewport_->focusOnSelectedObject(); -} - -void ViewportController::hideSelectedElements() { - viewport_->hideSelectedElements(); -} - -void ViewportController::isolateSelectedElements() { - viewport_->isolateSelectedElements(); -} - -void ViewportController::showAllElements() { - viewport_->showAllElements(); -} - -void ViewportController::invertSelection() { - viewport_->invertElementVisibility(); -} - -void ViewportController::updateVolumeReadout() { +void ViewportView::updateVolumeReadout() { if (viewport_->toolMode() != ViewportWindow::ToolMode::Volume) return; const auto& sel = viewport_->selection().selectionIds(); @@ -317,21 +229,4 @@ void ViewportController::updateVolumeReadout() { viewport_->setOverlayLabels(labels); } -void ViewportController::maybeGuessFederatedFalseOrigin(uint32_t mid) { - Federation* federation = session_state_->federation(); - SceneLoader* loader = session_state_->loader(); - if (!federation->filePath().isEmpty()) return; - - const FederatedFalseOrigin& current = federation->federatedFalseOrigin(); - const FederatedFalseOrigin defaults; - if (current.xyz != defaults.xyz || current.rz_deg != defaults.rz_deg) return; - - const Eigen::Matrix4d* placement = loader->firstPlacement(mid); - const ModelGeoref* georef = loader->modelGeoref(mid); - if (placement == nullptr || georef == nullptr) return; - - federation->setFederatedFalseOrigin(guessFederatedFalseOrigin( - *placement, *georef, federation->config())); -} - } // namespace ifcviewerfull::modules::viewport diff --git a/src/ifcviewer-full/modules/viewport/Controller.h b/src/ifcviewer-full/modules/viewport/View.h similarity index 73% rename from src/ifcviewer-full/modules/viewport/Controller.h rename to src/ifcviewer-full/modules/viewport/View.h index e9f5bb3a56..0182683e54 100644 --- a/src/ifcviewer-full/modules/viewport/Controller.h +++ b/src/ifcviewer-full/modules/viewport/View.h @@ -18,8 +18,8 @@ * * ********************************************************************************/ -#ifndef IFCINTERFACE_PANELS_VIEWPORT_CONTROLLER_H -#define IFCINTERFACE_PANELS_VIEWPORT_CONTROLLER_H +#ifndef IFCINTERFACE_MODULES_VIEWPORT_VIEW_H +#define IFCINTERFACE_MODULES_VIEWPORT_VIEW_H #include #include @@ -31,31 +31,25 @@ class LengthMeasurement; namespace ifcviewerfull::modules::viewport { -class ViewportController : public QObject { +// Renders SessionState into the OpenGL viewport. Subscribes to session-level +// signals only; on each one it calls refresh() to re-derive viewport state +// (false origin, per-model coord op + transformation + visibility) from +// SessionState idempotently. +// +// Also owns the stateful measurement tools and subscribes to viewport input +// events for them. That's a distinct concern from the state-render side but +// kept here to avoid a second tiny QObject. +class ViewportView : public QObject { Q_OBJECT public: - explicit ViewportController(ifcviewerfull::SessionState* session_state, - ViewportWindow* viewport, - QObject* parent = nullptr); - ~ViewportController() override; - - void applyFederatedFalseOrigin(); - void setHomeView(); - void goHomeView(); - void setFlyMode(); - void toggleSectionMode(); - void clearSectionPlanes(); - void toggleDistanceMode(); - void toggleAreaMode(); - void toggleVolumeMode(); - void focusSelectedObject(); - void hideSelectedElements(); - void isolateSelectedElements(); - void showAllElements(); - void invertSelection(); + explicit ViewportView(ifcviewerfull::SessionState* session_state, + ViewportWindow* viewport, + QObject* parent = nullptr); + ~ViewportView() override; private: + void refresh(); void applyCoordinateOperation(uint32_t mid); void applyModelTransformation(uint32_t mid); void applyModelVisibility(uint32_t mid);