mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 17:31:45 +00:00
Interface mockup 18
This commit is contained in:
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
+184
-214
@@ -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
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
-41
@@ -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
|
||||
+54
-159
@@ -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
|
||||
+16
-22
@@ -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);
|
||||
Reference in New Issue
Block a user