Interface mockup 18

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