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