diff --git a/src/ifcviewer-full/CMakeLists.txt b/src/ifcviewer-full/CMakeLists.txt
index ebe0a9b772..22ad90c3b6 100644
--- a/src/ifcviewer-full/CMakeLists.txt
+++ b/src/ifcviewer-full/CMakeLists.txt
@@ -1,3 +1,4 @@
+# This file was generated with the assistance of an AI coding tool.
################################################################################
# #
# This file is part of IfcOpenShell. #
@@ -19,19 +20,89 @@
message("Running CMakeLists.txt in /src/ifcviewer-full")
-file(GLOB IFCVIEWER_FULL_CPP_FILES ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp)
-file(GLOB IFCVIEWER_FULL_H_FILES ${CMAKE_CURRENT_SOURCE_DIR}/*.h)
-set(IFCVIEWER_FULL_FILES ${IFCVIEWER_FULL_CPP_FILES} ${IFCVIEWER_FULL_H_FILES})
+set(QT_VERSION 6 CACHE STRING "Qt version")
+find_package(Qt${QT_VERSION} COMPONENTS Core Gui Widgets Svg REQUIRED PATHS ${QT_DIR})
+
+set(IFCVIEWER_FULL_FILES
+ ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/ElementRegistry.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/ElementRegistry.h
+ ${CMAKE_CURRENT_SOURCE_DIR}/InterfaceSettings.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/InterfaceSettings.h
+ ${CMAKE_CURRENT_SOURCE_DIR}/SessionState.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/SessionState.h
+ ${CMAKE_CURRENT_SOURCE_DIR}/MainWindow.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/MainWindow.h
+ ${CMAKE_CURRENT_SOURCE_DIR}/Measurement.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/Measurement.h
+ ${CMAKE_CURRENT_SOURCE_DIR}/components/SvgIcon.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/components/SvgIcon.h
+ ${CMAKE_CURRENT_SOURCE_DIR}/components/Style.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/components/Style.h
+ ${CMAKE_CURRENT_SOURCE_DIR}/components/Dialog.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/components/Dialog.h
+ ${CMAKE_CURRENT_SOURCE_DIR}/components/Tabs.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/components/Tabs.h
+ ${CMAKE_CURRENT_SOURCE_DIR}/components/KeyValueTable.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/components/KeyValueTable.h
+ ${CMAKE_CURRENT_SOURCE_DIR}/components/Buttons.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/components/Buttons.h
+ ${CMAKE_CURRENT_SOURCE_DIR}/components/Section.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/components/Section.h
+ ${CMAKE_CURRENT_SOURCE_DIR}/components/Panel.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/components/Panel.h
+ ${CMAKE_CURRENT_SOURCE_DIR}/modules/models/AddModelDialog.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/modules/models/AddModelDialog.h
+ ${CMAKE_CURRENT_SOURCE_DIR}/modules/models/SettingsDialog.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/modules/models/SettingsDialog.h
+ ${CMAKE_CURRENT_SOURCE_DIR}/modules/models/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/Panel.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/modules/models/Panel.h
+ ${CMAKE_CURRENT_SOURCE_DIR}/modules/models/View.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/modules/models/View.h
+ ${CMAKE_CURRENT_SOURCE_DIR}/modules/todo/Panel.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/modules/todo/Panel.h
+ ${CMAKE_CURRENT_SOURCE_DIR}/modules/properties/Types.h
+ ${CMAKE_CURRENT_SOURCE_DIR}/modules/properties/Panel.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/modules/properties/Panel.h
+ ${CMAKE_CURRENT_SOURCE_DIR}/modules/properties/View.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/modules/properties/View.h
+ ${CMAKE_CURRENT_SOURCE_DIR}/modules/project/Controller.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/modules/project/Controller.h
+ ${CMAKE_CURRENT_SOURCE_DIR}/modules/settings/Dialog.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/modules/settings/Dialog.h
+ ${CMAKE_CURRENT_SOURCE_DIR}/modules/spatial_hierarchy/Types.h
+ ${CMAKE_CURRENT_SOURCE_DIR}/modules/spatial_hierarchy/Panel.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/modules/spatial_hierarchy/Panel.h
+ ${CMAKE_CURRENT_SOURCE_DIR}/modules/spatial_hierarchy/View.cpp
+ ${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}/interface_resources.qrc
+)
add_executable(IfcViewerFull ${IFCVIEWER_FULL_FILES})
set_target_properties(IfcViewerFull PROPERTIES
AUTOMOC ON
+ AUTORCC ON
WIN32_EXECUTABLE ON
MACOSX_BUNDLE ON
)
-target_link_libraries(IfcViewerFull PRIVATE IfcViewer)
+target_link_libraries(IfcViewerFull PRIVATE
+ IfcViewer
+ Qt${QT_VERSION}::Core
+ Qt${QT_VERSION}::Gui
+ Qt${QT_VERSION}::Svg
+ Qt${QT_VERSION}::Widgets
+)
install(TARGETS IfcViewerFull EXPORT ${IFCOPENSHELL_EXPORT_TARGETS})
ifcopenshell_deploy_qt_runtime(IfcViewerFull)
diff --git a/src/interface/ElementRegistry.cpp b/src/ifcviewer-full/ElementRegistry.cpp
similarity index 100%
rename from src/interface/ElementRegistry.cpp
rename to src/ifcviewer-full/ElementRegistry.cpp
diff --git a/src/interface/ElementRegistry.h b/src/ifcviewer-full/ElementRegistry.h
similarity index 100%
rename from src/interface/ElementRegistry.h
rename to src/ifcviewer-full/ElementRegistry.h
diff --git a/src/ifcviewer-full/FederationSettingsDialog.cpp b/src/ifcviewer-full/FederationSettingsDialog.cpp
deleted file mode 100644
index aa55eddeba..0000000000
--- a/src/ifcviewer-full/FederationSettingsDialog.cpp
+++ /dev/null
@@ -1,174 +0,0 @@
-/********************************************************************************
- * *
- * This file is part of IfcOpenShell. *
- * *
- * IfcOpenShell is free software: you can redistribute it and/or modify *
- * it under the terms of the Lesser GNU General Public License as published by *
- * the Free Software Foundation, either version 3.0 of the License, or *
- * (at your option) any later version. *
- * *
- * IfcOpenShell is distributed in the hope that it will be useful, *
- * but WITHOUT ANY WARRANTY; without even the implied warranty of *
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
- * Lesser GNU General Public License for more details. *
- * *
- * You should have received a copy of the Lesser GNU General Public License *
- * along with this program. If not, see . *
- * *
- ********************************************************************************/
-
-#include "FederationSettingsDialog.h"
-#include "Federation.h"
-
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-
-namespace {
-
-// Common length units users will pick. itemData() carries
-// (prefix, name) — empty prefix for non-prefixed or conversion-based units.
-struct UnitChoice {
- const char* label;
- const char* prefix;
- const char* name;
-};
-const UnitChoice kUnitChoices[] = {
- { "Metres (m)", "", "METRE" },
- { "Millimetres (mm)", "MILLI", "METRE" },
- { "Centimetres (cm)", "CENTI", "METRE" },
- { "Kilometres (km)", "KILO", "METRE" },
- { "Feet (ft)", "", "foot" },
- { "Inches (in)", "", "inch" },
- { "Yards (yd)", "", "yard" },
- { "Miles (mi)", "", "mile" },
-};
-
-} // namespace
-
-FederationSettingsDialog::FederationSettingsDialog(Federation* federation,
- QWidget* parent)
- : QDialog(parent), federation_(federation)
-{
- setWindowTitle("Federation Settings");
- setupUi();
-}
-
-void FederationSettingsDialog::setupUi() {
- auto* root = new QVBoxLayout(this);
-
- // Unit
- {
- auto* group = new QGroupBox("Federation Unit", this);
- auto* form = new QFormLayout(group);
- unit_combo_ = new QComboBox(group);
- for (const auto& uc : kUnitChoices) {
- QStringList data;
- data << QString::fromUtf8(uc.prefix) << QString::fromUtf8(uc.name);
- unit_combo_->addItem(uc.label, data);
- }
- unit_combo_->setToolTip(
- "All Federated False Origin and Model Transformation numbers "
- "are interpreted in this unit. Changing it after data has been "
- "entered re-interprets the numbers — same physical location "
- "expressed in the new unit.");
- form->addRow("Unit", unit_combo_);
- root->addWidget(group);
- }
-
- // Federated False Origin
- {
- auto* group = new QGroupBox("Federated False Origin", this);
- group->setToolTip(
- "Nominate a point in the federation that becomes the new (0,0,0). "
- "Optional Z-rotation aligns north for the federation.");
- auto* form = new QFormLayout(group);
-
- auto build_xyz = [this](QDoubleSpinBox*& sb) {
- sb = new QDoubleSpinBox();
- sb->setRange(-1e12, 1e12);
- sb->setDecimals(6);
- sb->setSingleStep(1.0);
- };
- build_xyz(xyz_x_);
- build_xyz(xyz_y_);
- build_xyz(xyz_z_);
- form->addRow("X", xyz_x_);
- form->addRow("Y", xyz_y_);
- form->addRow("Z", xyz_z_);
-
- rz_deg_ = new QDoubleSpinBox();
- rz_deg_->setRange(-360.0, 360.0);
- rz_deg_->setDecimals(6);
- rz_deg_->setSingleStep(1.0);
- form->addRow("Z rotation (°)", rz_deg_);
- root->addWidget(group);
- }
-
- auto* button_box = new QDialogButtonBox(
- QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
- root->addWidget(button_box);
- connect(button_box, &QDialogButtonBox::accepted, this,
- &FederationSettingsDialog::onAccepted);
- connect(button_box, &QDialogButtonBox::rejected, this, &QDialog::reject);
-}
-
-void FederationSettingsDialog::showEvent(QShowEvent* event) {
- syncFromFederation();
- QDialog::showEvent(event);
-}
-
-void FederationSettingsDialog::syncFromFederation() {
- if (!federation_) return;
-
- const auto& cfg = federation_->config();
- int idx = -1;
- for (int i = 0; i < unit_combo_->count(); ++i) {
- const QStringList data = unit_combo_->itemData(i).toStringList();
- if (data.size() == 2 &&
- data[0].toStdString() == cfg.unit_prefix &&
- data[1].toStdString() == cfg.unit_name) {
- idx = i;
- break;
- }
- }
- if (idx >= 0) {
- unit_combo_->setCurrentIndex(idx);
- } else {
- // Federation has a unit that isn't in the common list — fall back to
- // the first entry. Saving keeps whatever the user picks; the
- // original federation unit is preserved unless they hit Ok.
- unit_combo_->setCurrentIndex(0);
- }
-
- const auto& origin = federation_->federatedFalseOrigin();
- xyz_x_->setValue(origin.xyz.x());
- xyz_y_->setValue(origin.xyz.y());
- xyz_z_->setValue(origin.xyz.z());
- rz_deg_->setValue(origin.rz_deg);
-}
-
-void FederationSettingsDialog::onAccepted() {
- if (!federation_) { accept(); return; }
-
- const QStringList data = unit_combo_->currentData().toStringList();
- FederationConfig cfg;
- if (data.size() == 2) {
- cfg.unit_prefix = data[0].toStdString();
- cfg.unit_name = data[1].toStdString();
- }
- federation_->setConfig(cfg);
-
- FederatedFalseOrigin origin;
- origin.xyz = Eigen::Vector3d(xyz_x_->value(), xyz_y_->value(), xyz_z_->value());
- origin.rz_deg = rz_deg_->value();
- federation_->setFederatedFalseOrigin(origin);
-
- accept();
-}
diff --git a/src/ifcviewer-full/FederationSettingsDialog.h b/src/ifcviewer-full/FederationSettingsDialog.h
deleted file mode 100644
index 04cf8ebe3b..0000000000
--- a/src/ifcviewer-full/FederationSettingsDialog.h
+++ /dev/null
@@ -1,61 +0,0 @@
-/********************************************************************************
- * *
- * This file is part of IfcOpenShell. *
- * *
- * IfcOpenShell is free software: you can redistribute it and/or modify *
- * it under the terms of the Lesser GNU General Public License as published by *
- * the Free Software Foundation, either version 3.0 of the License, or *
- * (at your option) any later version. *
- * *
- * IfcOpenShell is distributed in the hope that it will be useful, *
- * but WITHOUT ANY WARRANTY; without even the implied warranty of *
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
- * Lesser GNU General Public License for more details. *
- * *
- * You should have received a copy of the Lesser GNU General Public License *
- * along with this program. If not, see . *
- * *
- ********************************************************************************/
-
-#ifndef FEDERATIONSETTINGSDIALOG_H
-#define FEDERATIONSETTINGSDIALOG_H
-
-#include
-
-QT_BEGIN_NAMESPACE
-class QComboBox;
-class QDoubleSpinBox;
-QT_END_NAMESPACE
-
-class Federation;
-
-// Edits federation-wide state: the display unit (used to interpret all
-// FederatedFalseOrigin / ModelTransformation numeric inputs) and the
-// FederatedFalseOrigin (XYZ + Z-axis rotation in that unit). On Ok,
-// calls Federation::setConfig + setFederatedFalseOrigin, which fire the
-// granular Federation signals and trigger the viewport to recompose.
-class FederationSettingsDialog : public QDialog {
- Q_OBJECT
-public:
- explicit FederationSettingsDialog(Federation* federation, QWidget* parent = nullptr);
-
-protected:
- void showEvent(QShowEvent* event) override;
-
-private:
- void setupUi();
- void syncFromFederation();
- void onAccepted();
-
- Federation* federation_ = nullptr;
-
- // The combobox encodes (prefix, name) pairs in user data; common length
- // units are listed. itemData(idx) returns a QStringList { prefix, name }.
- QComboBox* unit_combo_ = nullptr;
- QDoubleSpinBox* xyz_x_ = nullptr;
- QDoubleSpinBox* xyz_y_ = nullptr;
- QDoubleSpinBox* xyz_z_ = nullptr;
- QDoubleSpinBox* rz_deg_ = nullptr;
-};
-
-#endif
diff --git a/src/interface/InterfaceSettings.cpp b/src/ifcviewer-full/InterfaceSettings.cpp
similarity index 100%
rename from src/interface/InterfaceSettings.cpp
rename to src/ifcviewer-full/InterfaceSettings.cpp
diff --git a/src/interface/InterfaceSettings.h b/src/ifcviewer-full/InterfaceSettings.h
similarity index 100%
rename from src/interface/InterfaceSettings.h
rename to src/ifcviewer-full/InterfaceSettings.h
diff --git a/src/ifcviewer-full/MainWindow.cpp b/src/ifcviewer-full/MainWindow.cpp
index 61b379ce79..a85a6197df 100644
--- a/src/ifcviewer-full/MainWindow.cpp
+++ b/src/ifcviewer-full/MainWindow.cpp
@@ -1,3 +1,4 @@
+// This file was generated with the assistance of an AI coding tool.
/********************************************************************************
* *
* This file is part of IfcOpenShell. *
@@ -18,1390 +19,513 @@
********************************************************************************/
#include "MainWindow.h"
-#include "AppSettings.h"
-#include "Federation.h"
-#include "FederationSettingsDialog.h"
-#include "Measurement.h"
-#include "ModelTransformationDialog.h"
-#include "SettingsWindow.h"
-#include "LodBuilder.h"
-#include "SidecarCache.h"
-#include
-#include
+#include "../ifcviewer/AppSettings.h"
+#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/todo/Panel.h"
+#include "modules/models/View.h"
+#include "modules/models/Panel.h"
+#include "modules/project/Controller.h"
+#include "modules/properties/View.h"
+#include "modules/properties/Panel.h"
+#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/Panel.h"
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
#include
-#include
-#include
-#include
-#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace ifcinterface::shell {
MainWindow::MainWindow(QWidget* parent)
: QMainWindow(parent)
{
- setupUi();
- setupMenus();
-
federation_ = new Federation(this);
-
- // Federation -> viewport: granular signals so we recompose only what's
- // affected. Each handler reads the current federation state, composes
- // the matrix, and pushes to the viewport.
- connect(federation_, &Federation::federatedFalseOriginChanged,
- this, &MainWindow::applyFederatedFalseOriginToViewport);
- connect(federation_, &Federation::configChanged, this, [this]() {
- // Federation unit changed — both stage 3 (uses fed unit) and every
- // model's stage 4 (b/pivot are in fed units) need recomposing.
- applyFederatedFalseOriginToViewport();
- for (const auto& kv : fed_id_to_model_id_) {
- applyModelTransformationToViewport(kv.second);
- }
- });
- connect(federation_, &Federation::modelTransformationChanged,
- this, [this](const QString& fed_id) {
- auto it = fed_id_to_model_id_.find(fed_id);
- if (it != fed_id_to_model_id_.end()) {
- applyModelTransformationToViewport(it->second);
- }
- });
- connect(federation_, &Federation::modelVisibilityChanged,
- this, [this](const QString& fed_id, bool /*visible*/) {
- auto it = fed_id_to_model_id_.find(fed_id);
- if (it != fed_id_to_model_id_.end()) {
- applyModelVisibilityToViewport(it->second);
- }
- });
-
- connect(federation_, &Federation::modelGroupChanged,
- this, [this](const QString& fed_id, const QString& /*group_id*/) {
- auto it = fed_id_to_model_id_.find(fed_id);
- if (it == fed_id_to_model_id_.end()) return;
- reparentModelTreeRoot(it->second);
- // Effective visibility may have flipped because the new group's
- // chain visibility differs from the old one.
- applyModelVisibilityToViewport(it->second);
- });
-
- connect(federation_, &Federation::groupAdded,
- this, [this](const QString& group_id) {
- ensureGroupTreeItem(group_id);
- reparentGroupTreeItem(group_id);
- refreshGroupRowAppearance(group_id);
- });
-
- connect(federation_, &Federation::groupChanged,
- this, [this](const QString& group_id) {
- const Federation::Group* g = federation_->findGroupById(group_id);
- if (!g) return;
- auto it = group_tree_items_.find(group_id);
- if (it == group_tree_items_.end()) return;
- it->second->setText(0, g->display_name);
- reparentGroupTreeItem(group_id);
- // Reparenting a group changes the chain visibility for the moved
- // group + its descendants — refresh both rows and viewport.
- for (const QString& gid : descendantGroupIds(group_id)) {
- refreshGroupRowAppearance(gid);
- }
- applyVisibilityCascadeFromGroup(group_id);
- });
-
- connect(federation_, &Federation::groupVisibilityChanged,
- this, [this](const QString& group_id, bool /*visible*/) {
- for (const QString& gid : descendantGroupIds(group_id)) {
- refreshGroupRowAppearance(gid);
- }
- applyVisibilityCascadeFromGroup(group_id);
- });
-
- connect(federation_, &Federation::groupRemoved,
- this, [this](const QString& group_id) {
- auto it = group_tree_items_.find(group_id);
- if (it == group_tree_items_.end()) return;
- // By now, the federation has fired groupChanged / modelGroupChanged
- // for every direct child, and our slots have moved them out from
- // under this item, so deleting it just removes the (now empty)
- // group row.
- delete it->second;
- group_tree_items_.erase(it);
- });
-
+ element_registry_ = new ifcinterface::ElementRegistry(this);
+ session_state_ = new ifcinterface::SessionState(this);
+ session_state_->bindFederation(federation_);
+ session_state_->bindElementRegistry(element_registry_);
connect(federation_, &Federation::dirtyChanged, this, [this](bool dirty) {
setWindowModified(dirty);
+ updateWindowTitle();
+ });
+ connect(session_state_, &ifcinterface::SessionState::modelsChanged,
+ this, &MainWindow::updateWindowTitle);
+ connect(session_state_, &ifcinterface::SessionState::projectOpened,
+ this, [this](const QString&) { updateWindowTitle(); });
+ connect(session_state_, &ifcinterface::SessionState::projectSaved,
+ this, [this](const QString&) { updateWindowTitle(); });
+ connect(session_state_, &ifcinterface::SessionState::projectReset,
+ this, &MainWindow::updateWindowTitle);
+ setupChrome();
+ setupViewport();
+ setupPanels();
+ setupStatus();
+ setupLoader();
+ setupRibbon();
+ resize(1720, 980);
+}
+
+void MainWindow::setupChrome() {
+ setObjectName("appWindow");
+ setDockOptions(QMainWindow::AllowNestedDocks |
+ QMainWindow::AllowTabbedDocks |
+ QMainWindow::GroupedDragging);
+
+ auto bind_shortcut = [this](const QKeySequence& sequence, auto fn) {
+ auto* shortcut = new QShortcut(sequence, this);
+ shortcut->setContext(Qt::WindowShortcut);
+ connect(shortcut, &QShortcut::activated, this, fn);
+ };
+ bind_shortcut(QKeySequence("Ctrl+Shift+L"), [this]() {
+ viewport_controller_->toggleDistanceMode();
+ });
+ bind_shortcut(QKeySequence("Ctrl+Shift+A"), [this]() {
+ viewport_controller_->toggleAreaMode();
+ });
+ bind_shortcut(QKeySequence("Ctrl+Shift+V"), [this]() {
+ viewport_controller_->toggleVolumeMode();
+ });
+ bind_shortcut(QKeySequence(Qt::SHIFT | Qt::Key_F), [this]() {
+ viewport_controller_->setFlyMode();
+ });
+ bind_shortcut(QKeySequence(Qt::Key_K), [this]() {
+ viewport_controller_->toggleSectionMode();
+ });
+ bind_shortcut(QKeySequence(Qt::SHIFT | Qt::Key_K), [this]() {
+ viewport_controller_->clearSectionPlanes();
+ });
+ bind_shortcut(QKeySequence(Qt::Key_H), [this]() {
+ viewport_controller_->hideSelectedElements();
+ });
+ bind_shortcut(QKeySequence(Qt::SHIFT | Qt::Key_H), [this]() {
+ viewport_controller_->isolateSelectedElements();
+ });
+ bind_shortcut(QKeySequence(Qt::ALT | Qt::Key_H), [this]() {
+ viewport_controller_->showAllElements();
+ });
+ updateWindowTitle();
+}
+
+QToolButton* MainWindow::makePanelToggle(const QString& text, QDockWidget* dock) {
+ auto* button = components::buttons::makeButton(text, ":/icons/sidebar-expand.svg", this);
+ button->setCheckable(true);
+ button->setChecked(dock->isVisible());
+ connect(button, &QToolButton::toggled, dock, [dock](bool checked) {
+ dock->setVisible(checked);
+ if (checked) dock->raise();
+ });
+ connect(dock, &QDockWidget::visibilityChanged, button, [button](bool visible) {
+ const QSignalBlocker blocker(button);
+ button->setChecked(visible);
+ });
+ return button;
+}
+
+QWidget* MainWindow::buildHomeRibbonPage() {
+ auto* page = new QFrame(this);
+ page->setObjectName("ribbonPage");
+ auto* row = new QHBoxLayout(page);
+ row->setContentsMargins(6, 4, 6, 4);
+ row->setSpacing(0);
+
+ auto* new_project = components::buttons::makeButton("New Project", ":/icons/plus-square.svg", this);
+ connect(new_project, &QToolButton::clicked, this, [this]() {
+ project_controller_->newProject();
+ });
+ auto* open_project = components::buttons::makeButton("Open Project", ":/icons/download-square.svg", this);
+ connect(open_project, &QToolButton::clicked, this, [this]() {
+ project_controller_->openProject();
+ });
+ auto* open_cloud = components::buttons::makeButton("Open Cloud", ":/icons/cloud-square.svg", this);
+ connect(open_cloud, &QToolButton::clicked, this, [this]() {
+ session_state_->setStatusMessage("Project", "Open Cloud Project coming soon");
+ });
+ auto* open_recent = components::buttons::makeButton("Open Recent", ":/icons/clock-rotate-right.svg", this);
+ connect(open_recent, &QToolButton::clicked, this, [this]() {
+ session_state_->setStatusMessage("Project", "Open Recent coming soon");
+ });
+ auto* save_project = components::buttons::makeButton("Save Project", ":/icons/floppy-disk.svg", this);
+ connect(save_project, &QToolButton::clicked, this, [this]() {
+ project_controller_->saveProject();
+ });
+ auto* save_project_as = components::buttons::makeButton("Save As", ":/icons/floppy-disk-arrow-in.svg", this);
+ connect(save_project_as, &QToolButton::clicked, this, [this]() {
+ project_controller_->saveProjectAs();
});
- loader_ = new SceneLoader(viewport_, this);
- connect(loader_, &SceneLoader::loadStarted,
- this, &MainWindow::onLoadStarted);
- connect(loader_, &SceneLoader::progressChanged,
- this, &MainWindow::onLoadProgressChanged);
- connect(loader_, &SceneLoader::sidecarElementsReady,
- this, &MainWindow::onSidecarElementsReady);
- connect(loader_, &SceneLoader::loadedFromSidecar,
- this, &MainWindow::onLoadedFromSidecar);
- connect(loader_, &SceneLoader::dataSourceReady,
- this, &MainWindow::onDataSourceReady);
- connect(loader_, &SceneLoader::streamedElementsReady,
- this, &MainWindow::onStreamedElementsReady);
- connect(loader_, &SceneLoader::loadedFromStream,
- this, &MainWindow::onLoadedFromStream);
- connect(loader_, &SceneLoader::loadCancelled,
- this, &MainWindow::onLoadCancelled);
- connect(loader_, &SceneLoader::loadError,
- this, &MainWindow::onLoadError);
- connect(loader_, &SceneLoader::allLoadsFinished,
- this, &MainWindow::onAllLoadsFinished);
+ auto* add_model = components::buttons::makeButton("Add Model", ":/icons/cube.svg", this);
+ connect(add_model, &QToolButton::clicked, this, [this]() {
+ models_controller_->addFiles();
+ });
+ auto* sync_models = components::buttons::makeButton("Sync Models", ":/icons/refresh-double.svg", this);
+ connect(sync_models, &QToolButton::clicked, this, [this]() {
+ session_state_->setStatusMessage("Models", "Sync models coming soon");
+ });
- connect(viewport_, &ViewportWindow::frameStatsUpdated, this,
+ auto* settings_button = components::buttons::makeButton("Settings", ":/icons/settings.svg", this);
+ connect(settings_button, &QToolButton::clicked, this, [this]() {
+ modules::settings::SettingsDialog dialog(this);
+ dialog.exec();
+ });
+
+ row->addWidget(components::buttons::makeButtonGroup("PROJECT", {new_project, open_project, open_cloud, open_recent, save_project, save_project_as}, this));
+ row->addWidget(components::buttons::makeButtonGroup("MODELS", {add_model, sync_models}, this));
+ row->addWidget(components::buttons::makeButtonGroup("SETTINGS", {settings_button}, this));
+ row->addStretch(1);
+ return page;
+}
+
+QWidget* MainWindow::buildNavigateRibbonPage() {
+ auto* page = new QFrame(this);
+ page->setObjectName("ribbonPage");
+ auto* row = new QHBoxLayout(page);
+ row->setContentsMargins(2, 4, 2, 4);
+ row->setSpacing(0);
+
+ auto* set_home = components::buttons::makeButton("Set Home", ":/icons/home.svg", this);
+ connect(set_home, &QToolButton::clicked, this, [this]() {
+ viewport_controller_->setHomeView();
+ });
+ auto* go_home = components::buttons::makeButton("Go Home", ":/icons/home-alt.svg", this);
+ connect(go_home, &QToolButton::clicked, this, [this]() {
+ viewport_controller_->goHomeView();
+ });
+ auto* view_all = components::buttons::makeButton("View All", ":/icons/cube-scan.svg", this);
+ connect(view_all, &QToolButton::clicked, this, [this]() {
+ if (viewport_widget_) viewport_widget_->viewport()->viewAll();
+ });
+ auto* view_selected = components::buttons::makeButton("View Selected", ":/icons/cube-scan-solid.svg", this);
+ connect(view_selected, &QToolButton::clicked, this, [this]() {
+ viewport_controller_->focusSelectedObject();
+ });
+
+ auto* plan_view = components::buttons::makeButton("Plan", ":/icons/planimetry.svg", this);
+ connect(plan_view, &QToolButton::clicked, this, [this]() {
+ if (viewport_widget_) viewport_widget_->viewport()->setStandardView(90.0f, 90.0f);
+ });
+ auto* front_view = components::buttons::makeButton("Front", ":/icons/city.svg", this);
+ connect(front_view, &QToolButton::clicked, this, [this]() {
+ if (viewport_widget_) viewport_widget_->viewport()->setStandardView(0.0f, 0.0f);
+ });
+ auto* side_view = components::buttons::makeButton("Side", ":/icons/building.svg", this);
+ connect(side_view, &QToolButton::clicked, this, [this]() {
+ if (viewport_widget_) viewport_widget_->viewport()->setStandardView(90.0f, 0.0f);
+ });
+ auto* align_object = components::buttons::makeButton("Align Object", ":/icons/cellar.svg", this);
+ connect(align_object, &QToolButton::clicked, this, [this]() {
+ session_state_->setStatusMessage("Orientation", "Align to object coming soon");
+ });
+ auto* projection_button = components::buttons::makeButton("Perspective", ":/icons/perspective-view.svg", this);
+ connect(projection_button, &QToolButton::clicked, this, [this, projection_button]() {
+ if (!viewport_widget_) return;
+ viewport_widget_->viewport()->toggleProjection();
+ projection_button->setText(
+ viewport_widget_->viewport()->projectionOrtho() ? "Ortho" : "Perspective");
+ });
+
+ auto* fly_mode = components::buttons::makeButton("Fly", ":/icons/drone.svg", this);
+ connect(fly_mode, &QToolButton::clicked, this, [this]() {
+ viewport_controller_->setFlyMode();
+ });
+ auto* section_mode = components::buttons::makeButton("Section", ":/icons/cube-cut-with-curve.svg", this);
+ connect(section_mode, &QToolButton::clicked, this, [this]() {
+ viewport_controller_->toggleSectionMode();
+ });
+
+ row->addWidget(components::buttons::makeButtonGroup("CAMERA", {set_home, go_home, view_all, view_selected}, this));
+ row->addWidget(components::buttons::makeButtonGroup("ORIENTATION", {plan_view, front_view, side_view, align_object, projection_button}, this));
+ row->addWidget(components::buttons::makeButtonGroup("MODE", {fly_mode, section_mode}, this));
+ row->addStretch(1);
+ return page;
+}
+
+QWidget* MainWindow::buildInspectRibbonPage() {
+ auto* page = new QFrame(this);
+ page->setObjectName("ribbonPage");
+ auto* row = new QHBoxLayout(page);
+ row->setContentsMargins(2, 4, 2, 4);
+ row->setSpacing(0);
+
+ auto* hide_selected = components::buttons::makeButton("Hide", ":/icons/eye-closed.svg", this);
+ connect(hide_selected, &QToolButton::clicked, this, [this]() {
+ viewport_controller_->hideSelectedElements();
+ });
+ auto* isolate_selected = components::buttons::makeButton("Isolate", ":/icons/eye-solid.svg", this);
+ connect(isolate_selected, &QToolButton::clicked, this, [this]() {
+ viewport_controller_->isolateSelectedElements();
+ });
+ auto* show_all = components::buttons::makeButton("Show All", ":/icons/eye.svg", this);
+ connect(show_all, &QToolButton::clicked, this, [this]() {
+ viewport_controller_->showAllElements();
+ });
+ auto* invert_selection = components::buttons::makeButton("Invert", ":/icons/intersect.svg", this);
+ connect(invert_selection, &QToolButton::clicked, this, [this]() {
+ viewport_controller_->invertSelection();
+ });
+
+ auto* distance = components::buttons::makeButton("Distance", ":/icons/select-edge3d.svg", this);
+ connect(distance, &QToolButton::clicked, this, [this]() {
+ viewport_controller_->toggleDistanceMode();
+ });
+ auto* area = components::buttons::makeButton("Area", ":/icons/select-face3d.svg", this);
+ connect(area, &QToolButton::clicked, this, [this]() {
+ viewport_controller_->toggleAreaMode();
+ });
+ auto* volume = components::buttons::makeButton("Volume", ":/icons/select-point3d.svg", this);
+ connect(volume, &QToolButton::clicked, this, [this]() {
+ viewport_controller_->toggleVolumeMode();
+ });
+
+ row->addWidget(components::buttons::makeButtonGroup("SELECTION", {hide_selected, isolate_selected, show_all, invert_selection}, this));
+ row->addWidget(components::buttons::makeButtonGroup("MEASURE", {distance, area, volume}, this));
+ row->addStretch(1);
+ return page;
+}
+
+QWidget* MainWindow::buildPanelsRibbonPage() {
+ auto* page = new QFrame(this);
+ page->setObjectName("ribbonPage");
+ auto* row = new QHBoxLayout(page);
+ row->setContentsMargins(2, 4, 2, 4);
+ row->setSpacing(0);
+
+ row->addWidget(components::buttons::makeButtonGroup("DATA", {
+ makePanelToggle("Models", models_panel_),
+ makePanelToggle("Spatial", spatial_panel_),
+ makePanelToggle("Layers", layers_panel_),
+ makePanelToggle("Properties", properties_panel_)
+ }, this));
+ row->addWidget(components::buttons::makeButtonGroup("QUERY", {
+ makePanelToggle("Views", stored_views_panel_),
+ makePanelToggle("Search", search_panel_),
+ makePanelToggle("Sheets", spreadsheet_panel_),
+ makePanelToggle("Audit", audit_panel_)
+ }, this));
+ row->addWidget(components::buttons::makeButtonGroup("COLLABORATE", {
+ makePanelToggle("Clash", clash_panel_),
+ makePanelToggle("Issues", issues_panel_)
+ }, this));
+ row->addStretch(1);
+ return page;
+}
+
+void MainWindow::setupRibbon() {
+ auto* shell = new QFrame(this);
+
+ auto* shell_layout = new QVBoxLayout(shell);
+ shell_layout->setContentsMargins(0, 0, 0, 0);
+ shell_layout->setSpacing(0);
+
+ ribbon_tabs_ = new components::TabBar(shell);
+ ribbon_tabs_->addTab("Home");
+ ribbon_tabs_->addTab("Navigate");
+ ribbon_tabs_->addTab("Inspect");
+ ribbon_tabs_->addTab("Panels");
+ ribbon_tabs_->setCurrentIndex(0);
+
+ auto* ribbon_band = new QFrame(shell);
+ ribbon_band->setObjectName("ribbonBand");
+ auto* band_layout = new QVBoxLayout(ribbon_band);
+ band_layout->setContentsMargins(0, 0, 0, 0);
+ band_layout->setSpacing(0);
+
+ ribbon_pages_ = new QStackedWidget(ribbon_band);
+ ribbon_pages_->addWidget(buildHomeRibbonPage());
+ ribbon_pages_->addWidget(buildNavigateRibbonPage());
+ ribbon_pages_->addWidget(buildInspectRibbonPage());
+ ribbon_pages_->addWidget(buildPanelsRibbonPage());
+
+ band_layout->addWidget(ribbon_pages_);
+ shell_layout->addWidget(ribbon_tabs_);
+ shell_layout->addWidget(ribbon_band);
+
+ connect(ribbon_tabs_, &components::TabBar::currentChanged,
+ ribbon_pages_, &QStackedWidget::setCurrentIndex);
+
+ setMenuWidget(shell);
+}
+
+void MainWindow::setupViewport() {
+ viewport_widget_ = new modules::viewport::ViewportPanel(this);
+ setCentralWidget(viewport_widget_);
+}
+
+void MainWindow::setupPanels() {
+ models_panel_ = new modules::models::ModelsPanel(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);
+
+ layers_panel_ = new components::Panel("Layers", new modules::todo::TodoPanel("Layers", this), this);
+ stored_views_panel_ = new components::Panel(
+ "Stored Views", new modules::todo::TodoPanel("Stored Views", this), this);
+ search_panel_ = new components::Panel(
+ "Search and Query", new modules::todo::TodoPanel("Search and Query", this), this);
+ spreadsheet_panel_ = new components::Panel(
+ "Spreadsheet", new modules::todo::TodoPanel("Spreadsheet", this), this);
+ audit_panel_ = new components::Panel("Audit", new modules::todo::TodoPanel("Audit", this), this);
+ clash_panel_ = new components::Panel("Clash", new modules::todo::TodoPanel("Clash", this), this);
+ issues_panel_ = new components::Panel("Issues", new modules::todo::TodoPanel("Issues", this), this);
+
+ addDockWidget(Qt::LeftDockWidgetArea, models_panel_);
+ addDockWidget(Qt::LeftDockWidgetArea, spatial_panel_);
+ splitDockWidget(models_panel_, spatial_panel_, Qt::Vertical);
+
+ addDockWidget(Qt::RightDockWidgetArea, properties_panel_);
+ addDockWidget(Qt::RightDockWidgetArea, layers_panel_);
+ addDockWidget(Qt::RightDockWidgetArea, stored_views_panel_);
+ addDockWidget(Qt::RightDockWidgetArea, search_panel_);
+ addDockWidget(Qt::RightDockWidgetArea, spreadsheet_panel_);
+ addDockWidget(Qt::RightDockWidgetArea, audit_panel_);
+ addDockWidget(Qt::RightDockWidgetArea, clash_panel_);
+ addDockWidget(Qt::RightDockWidgetArea, issues_panel_);
+
+ tabifyDockWidget(properties_panel_, layers_panel_);
+ tabifyDockWidget(layers_panel_, stored_views_panel_);
+ tabifyDockWidget(stored_views_panel_, search_panel_);
+ tabifyDockWidget(search_panel_, spreadsheet_panel_);
+ tabifyDockWidget(spreadsheet_panel_, audit_panel_);
+ tabifyDockWidget(audit_panel_, clash_panel_);
+ tabifyDockWidget(clash_panel_, issues_panel_);
+ properties_panel_->raise();
+
+ layers_panel_->hide();
+ stored_views_panel_->hide();
+ search_panel_->hide();
+ spreadsheet_panel_->hide();
+ audit_panel_->hide();
+ clash_panel_->hide();
+ issues_panel_->hide();
+
+ resizeDocks({models_panel_, properties_panel_}, {290, 330}, Qt::Horizontal);
+ resizeDocks({models_panel_, spatial_panel_}, {280, 240}, Qt::Vertical);
+}
+
+void MainWindow::setupStatus() {
+ status_mode_label_ = new QLabel("Ready", this);
+ status_selection_label_ = new QLabel("No selection", this);
+ status_perf_label_ = new QLabel(this);
+ status_progress_bar_ = new QProgressBar(this);
+ status_perf_label_->setVisible(AppSettings::instance().showStats());
+ status_progress_bar_->setMaximumWidth(200);
+ status_progress_bar_->setVisible(false);
+
+ statusBar()->setSizeGripEnabled(false);
+ statusBar()->addWidget(status_mode_label_);
+ statusBar()->addWidget(status_selection_label_, 1);
+ statusBar()->addPermanentWidget(status_perf_label_);
+ statusBar()->addPermanentWidget(status_progress_bar_);
+
+ connect(&AppSettings::instance(), &AppSettings::showStatsChanged, this, [this](bool show) {
+ status_perf_label_->setVisible(show);
+ if (!show) status_perf_label_->clear();
+ });
+ connect(session_state_, &ifcinterface::SessionState::statusMessageChanged,
+ this, [this](const QString& mode, const QString& detail) {
+ status_mode_label_->setText(mode);
+ status_selection_label_->setText(detail);
+ });
+ session_state_->setStatusMessage("Ready", "No selection");
+}
+
+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_, viewport_widget_->viewport(), this);
+ project_controller_ = new modules::project::ProjectController(
+ this, federation_, session_state_, element_registry_,
+ viewport_widget_->viewport(), models_controller_, viewport_controller_, this);
+
+ connect(loader_, &SceneLoader::loadStarted, this,
+ [this](uint32_t /*mid*/, const QString& /*display_name*/) {
+ status_progress_bar_->setValue(0);
+ status_progress_bar_->setVisible(true);
+ });
+ connect(loader_, &SceneLoader::progressChanged, this,
+ [this](int percent) {
+ status_progress_bar_->setValue(percent);
+ });
+
+ auto hide_progress = [this]() {
+ 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();
+ });
+
+ connect(viewport_widget_->viewport(), &ViewportWindow::frameStatsUpdated, this,
[this](const ViewportWindow::FrameStats& s) {
- if (!stats_label_->isVisible()) return;
- stats_label_->setText(
- QString("%1 fps | %2 ms | %3/%4 obj | %5/%6 tri | %7 gl_draws (%8 sub)")
+ if (!status_perf_label_->isVisible()) return;
+ status_perf_label_->setText(
+ QString("%1 fps | %2 ms | %3/%4 obj | %5/%6 tri | %7 draws")
.arg(s.fps, 0, 'f', 1)
.arg(s.frame_time_ms, 0, 'f', 1)
.arg(s.visible_objects)
.arg(s.total_objects)
.arg(s.visible_triangles)
.arg(s.total_triangles)
- .arg(s.gl_draw_calls)
- .arg(s.indirect_sub_draws));
+ .arg(s.gl_draw_calls));
});
-
- connect(&AppSettings::instance(), &AppSettings::showStatsChanged, this, [this](bool show) {
- stats_label_->setVisible(show);
- if (!show) stats_label_->clear();
+ connect(viewport_widget_->viewport(), &ViewportWindow::objectPicked,
+ this, [this](uint32_t object_id) {
+ session_state_->setSelectedObjectId(object_id);
+ session_state_->notifySelectionChanged();
});
-
- updateWindowTitle();
- resize(1400, 900);
-}
-
-MainWindow::~MainWindow() = default;
-
-void MainWindow::setupUi() {
- viewport_ = new ViewportWindow();
- viewport_container_ = QWidget::createWindowContainer(viewport_, this);
- viewport_container_->setMinimumSize(400, 300);
- viewport_container_->setFocusPolicy(Qt::StrongFocus);
- setCentralWidget(viewport_container_);
-
- connect(viewport_, &ViewportWindow::objectPicked, this, &MainWindow::onObjectPicked);
- connect(viewport_, &ViewportWindow::surfacePickedInTool, this,
- [this](int x, int y, int modifiers) {
- const bool alt = (modifiers & Qt::AltModifier) != 0;
- switch (viewport_->toolMode()) {
- case ViewportWindow::ToolMode::Area:
- area_measurement_.onPick(*viewport_, x, y, alt);
- viewport_->setHudText(QString("Area: %1 m² (%2 tris)")
- .arg(area_measurement_.totalArea(), 0, 'f', 4)
- .arg(area_measurement_.triangleCount()));
- break;
- case ViewportWindow::ToolMode::Length:
- length_measurement_.onPick(*viewport_, x, y, alt);
- break;
- case ViewportWindow::ToolMode::None:
- break;
- }
- });
- connect(viewport_, &ViewportWindow::toolModeChanged, this,
- [this](ViewportWindow::ToolMode mode) {
- // Always clear both — the previous tool's accumulator/overlay
- // shouldn't bleed into the next one.
- area_measurement_.clear(*viewport_);
- length_measurement_.clear(*viewport_);
- switch (mode) {
- case ViewportWindow::ToolMode::Area:
- viewport_->setHudText("Area: 0.0000 m² (0 tris)");
- status_label_->setText("Area tool: LMB add, Alt+LMB single tri, click again to remove, Esc exits");
- break;
- case ViewportWindow::ToolMode::Length:
- viewport_->setHudText("Length tool: click first point");
- status_label_->setText("Length tool: LMB add point, Backspace remove last, Esc exits");
- break;
- case ViewportWindow::ToolMode::Volume:
- // Volume tool is passive — selection works as in None. The
- // readout helper writes both HUD and per-object labels for
- // whatever's currently selected, then keeps them in sync as
- // the selection changes.
- status_label_->setText("Volume tool: click / box-select objects, Esc exits");
- updateVolumeReadout();
- break;
- case ViewportWindow::ToolMode::None:
- viewport_->setHudText(QString());
- viewport_->setOverlayLabels({});
- status_label_->setText("Ready");
- break;
- }
- });
- connect(viewport_, &ViewportWindow::toolBackspacePressed, this, [this]() {
- if (viewport_->toolMode() == ViewportWindow::ToolMode::Length) {
- length_measurement_.removeLastPoint(*viewport_);
- }
- });
-
- auto* tree_dock = new QDockWidget("Elements", this);
- tree_dock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea);
- element_tree_ = new QTreeWidget();
- element_tree_->setHeaderLabels({"Name", "Type", "GUID"});
- element_tree_->setColumnWidth(0, 200);
- element_tree_->setColumnWidth(1, 120);
- element_tree_->setSelectionMode(QAbstractItemView::SingleSelection);
- element_tree_->setContextMenuPolicy(Qt::CustomContextMenu);
- connect(element_tree_, &QTreeWidget::itemSelectionChanged, this, &MainWindow::onTreeSelectionChanged);
- connect(element_tree_, &QTreeWidget::customContextMenuRequested,
- this, &MainWindow::onTreeContextMenu);
- tree_dock->setWidget(element_tree_);
- addDockWidget(Qt::LeftDockWidgetArea, tree_dock);
-
- auto* prop_dock = new QDockWidget("Properties", this);
- prop_dock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea);
- property_table_ = new QTableWidget();
- property_table_->setColumnCount(2);
- property_table_->setHorizontalHeaderLabels({"Property", "Value"});
- property_table_->horizontalHeader()->setStretchLastSection(true);
- property_table_->setEditTriggers(QAbstractItemView::NoEditTriggers);
- property_table_->setSelectionBehavior(QAbstractItemView::SelectRows);
- prop_dock->setWidget(property_table_);
- addDockWidget(Qt::RightDockWidgetArea, prop_dock);
-
- progress_bar_ = new QProgressBar();
- progress_bar_->setMaximumWidth(200);
- progress_bar_->setVisible(false);
- status_label_ = new QLabel("Ready");
- stats_label_ = new QLabel();
- stats_label_->setVisible(AppSettings::instance().showStats());
- statusBar()->addWidget(status_label_, 1);
- statusBar()->addPermanentWidget(stats_label_);
- statusBar()->addPermanentWidget(progress_bar_);
-}
-
-void MainWindow::setupMenus() {
- auto* file_menu = menuBar()->addMenu("&File");
- file_menu->addAction("&New Federation",
- this, &MainWindow::onFederationNew,
- QKeySequence::New);
- file_menu->addAction("&Open Federation...",
- this, &MainWindow::onFederationOpen,
- QKeySequence::Open);
- file_menu->addSeparator();
- file_menu->addAction("&Add Files...",
- this, &MainWindow::onFileOpen,
- QKeySequence("Ctrl+Shift+O"));
- file_menu->addAction("Add &Database...", this, &MainWindow::onDatabaseOpen);
- file_menu->addSeparator();
- file_menu->addAction("&Save Federation",
- this, &MainWindow::onFederationSave,
- QKeySequence::Save);
- file_menu->addAction("Save Federation &As...",
- this, &MainWindow::onFederationSaveAs,
- QKeySequence::SaveAs);
- file_menu->addAction("Federation Se&ttings...",
- this, &MainWindow::onFederationSettings);
- file_menu->addAction("&Model Transformations...",
- this, &MainWindow::onModelTransformations);
- file_menu->addSeparator();
- file_menu->addAction("&Settings...", this, &MainWindow::onFileSettings);
- file_menu->addSeparator();
- file_menu->addAction("&Quit", QKeySequence::Quit, qApp, &QApplication::quit);
-
- auto* view_menu = menuBar()->addMenu("&View");
- // F frames the current selection. The viewport already binds F in its
- // own keyPressEvent for the case where it has focus; this duplicate at
- // window level is so the shortcut still fires when the tree, property
- // table, or any other child widget has keyboard focus.
- view_menu->addAction("&Frame Selected", this, [this]() {
- viewport_->focusOnSelectedObject();
- }, QKeySequence(Qt::Key_F));
- view_menu->addAction("Print Selected &Coords", this, [this]() {
- viewport_->printSelectedObjectCoords();
- }, QKeySequence("Ctrl+Shift+P"));
- view_menu->addAction("&Measure Area", this, [this]() {
- viewport_->toggleAreaTool();
- }, QKeySequence("Ctrl+Shift+A"));
- view_menu->addAction("Measure &Length", this, [this]() {
- viewport_->toggleLengthTool();
- }, QKeySequence("Ctrl+Shift+L"));
- view_menu->addAction("Measure &Volume", this, [this]() {
- viewport_->toggleVolumeTool();
- }, QKeySequence("Ctrl+Shift+V"));
- view_menu->addSeparator();
- // Element visibility — H hides the current selection, Shift+H
- // isolates it (hides everything else within visible models), Alt+H
- // restores every element. Model-level hiding is independent and
- // stays put.
- view_menu->addAction("&Hide Selected", this, [this]() {
- viewport_->hideSelectedElements();
- }, QKeySequence(Qt::Key_H));
- view_menu->addAction("&Isolate Selected", this, [this]() {
- viewport_->isolateSelectedElements();
- }, QKeySequence(Qt::SHIFT | Qt::Key_H));
- view_menu->addAction("&Show All Elements", this, [this]() {
- viewport_->showAllElements();
- }, QKeySequence(Qt::ALT | Qt::Key_H));
- view_menu->addSeparator();
- view_menu->addAction("Set &Home View", this, &MainWindow::onSetHomeView);
- view_menu->addAction("&Go to Home View", this, &MainWindow::onGoHomeView);
-}
-
-void MainWindow::onFileOpen() {
- QStringList paths = QFileDialog::getOpenFileNames(
- this, "Add IFC Files", QString(),
- "IFC Files (*.ifc *.ifcxml *.ifczip);;"
- "IFC Viewer Cache (*.ifcview);;"
- "All Files (*)");
- if (!paths.isEmpty()) {
- addFiles(paths);
- }
-}
-
-void MainWindow::onDatabaseOpen() {
- QFileDialog dialog(this, "Add IFC Databases");
- dialog.setFileMode(QFileDialog::Directory);
- dialog.setOption(QFileDialog::ShowDirsOnly, true);
- dialog.setOption(QFileDialog::DontResolveSymlinks, true);
- // Native dialogs only support single-directory selection — use Qt's
- // dialog so we can flip the inner views into ExtendedSelection.
- dialog.setOption(QFileDialog::DontUseNativeDialog, true);
- if (auto* list = dialog.findChild("listView")) {
- list->setSelectionMode(QAbstractItemView::ExtendedSelection);
- }
- if (auto* tree = dialog.findChild()) {
- tree->setSelectionMode(QAbstractItemView::ExtendedSelection);
- }
- if (dialog.exec() != QDialog::Accepted) return;
- QStringList paths = dialog.selectedFiles();
- if (!paths.isEmpty()) {
- addFiles(paths);
- }
-}
-
-void MainWindow::onFileSettings() {
- if (settings_ == nullptr) {
- settings_ = new SettingsWindow(this);
- }
- settings_->open();
- settings_->activateWindow();
- settings_->raise();
-}
-
-void MainWindow::onFederationSettings() {
- if (federation_settings_ == nullptr) {
- federation_settings_ = new FederationSettingsDialog(federation_, this);
- }
- federation_settings_->open();
- federation_settings_->activateWindow();
- federation_settings_->raise();
-}
-
-void MainWindow::onModelTransformations() {
- if (model_transformations_ == nullptr) {
- model_transformations_ = new ModelTransformationDialog(federation_, this);
- }
- model_transformations_->open();
- model_transformations_->activateWindow();
- model_transformations_->raise();
-}
-
-void MainWindow::addFiles(const QStringList& paths) {
- QStringList accepted_paths;
- QStringList accepted_fed_ids;
- for (const auto& p : paths) {
- QString fed_id = federation_->addModel(p);
- if (fed_id.isEmpty()) continue; // .ifcfed or empty path — silently skipped
- accepted_paths << p;
- accepted_fed_ids << fed_id;
- }
- loadModelsFromPaths(accepted_paths, accepted_fed_ids);
- updateWindowTitle();
-}
-
-void MainWindow::loadModelsFromPaths(const QStringList& paths,
- const QStringList& fed_ids) {
- if (paths.isEmpty()) return;
- auto ids = loader_->addFiles(paths);
- for (int i = 0; i < paths.size() && i < static_cast(ids.size()); ++i) {
- uint32_t mid = ids[i];
- const QString& fed_id = fed_ids[i];
- fed_id_to_model_id_[fed_id] = mid;
- model_id_to_fed_id_[mid] = fed_id;
-
- QString display = QFileInfo(paths[i]).fileName();
- auto* root = new QTreeWidgetItem();
- root->setText(0, display);
- root->setText(1, "IFC Model");
- root->setData(0, Qt::UserRole, static_cast(0));
- tree_roots_[mid] = root;
- reparentModelTreeRoot(mid);
- }
-}
-
-bool MainWindow::openFederation(const QString& path) {
- if (!confirmDiscardIfDirty()) return false;
-
- QStringList warnings;
- QString err;
- if (!federation_->load(path, &warnings, &err)) {
- QMessageBox::warning(this, "Open Federation",
- QString("Could not open federation:\n%1").arg(err));
- return false;
- }
-
- clearScene();
-
- // Materialise group tree items. allGroups() walks parents-before-
- // children, so reparenting in the same pass always finds the parent's
- // tree item ready.
- for (const Federation::Group* g : federation_->allGroups()) {
- ensureGroupTreeItem(g->id);
- reparentGroupTreeItem(g->id);
- refreshGroupRowAppearance(g->id);
- }
-
- QStringList paths;
- QStringList fed_ids;
- QStringList missing;
- for (const auto& m : federation_->models()) {
- if (m.source_kind != "local") continue; // already warned by load()
- if (!QFileInfo::exists(m.source_path)) {
- missing << m.source_path;
- continue;
- }
- paths << m.source_path;
- fed_ids << m.id;
- }
- loadModelsFromPaths(paths, fed_ids);
-
- for (const auto& msg : missing) {
- warnings << QString("Source not found, kept in federation: %1").arg(msg);
- }
- if (!warnings.isEmpty()) {
- QMessageBox::warning(this, "Open Federation",
- "Federation opened with warnings:\n\n" + warnings.join("\n"));
- }
-
- federation_->markClean();
- updateWindowTitle();
-
- // Push the federation's loaded FederatedFalseOrigin to the viewport.
- // Per-model ModelTransformations get pushed as each model finishes
- // loading, via applyCoordinateOperationToViewport.
- applyFederatedFalseOriginToViewport();
-
- if (federation_->hasHomeView()) {
- const auto& hv = federation_->homeView();
- viewport_->setCamera(hv.target.x(), hv.target.y(), hv.target.z(),
- hv.distance, hv.yaw, hv.pitch);
- }
- return true;
-}
-
-void MainWindow::onFederationNew() {
- if (!confirmDiscardIfDirty()) return;
- clearScene();
- federation_->clear();
- updateWindowTitle();
-}
-
-void MainWindow::onFederationOpen() {
- QString path = QFileDialog::getOpenFileName(
- this, "Open Federation", QString(),
- "IFC Federation (*.ifcfed);;All Files (*)");
- if (path.isEmpty()) return;
- openFederation(path);
-}
-
-bool MainWindow::onFederationSave() {
- if (federation_->filePath().isEmpty()) return onFederationSaveAs();
- QString err;
- if (!federation_->save(federation_->filePath(), &err)) {
- QMessageBox::warning(this, "Save Federation",
- QString("Could not save federation:\n%1").arg(err));
- return false;
- }
- updateWindowTitle();
- return true;
-}
-
-bool MainWindow::onFederationSaveAs() {
- QString suggested = federation_->filePath();
- if (suggested.isEmpty()) suggested = "federation.ifcfed";
- QString path = QFileDialog::getSaveFileName(
- this, "Save Federation As", suggested,
- "IFC Federation (*.ifcfed);;All Files (*)");
- if (path.isEmpty()) return false;
- if (!path.endsWith(".ifcfed", Qt::CaseInsensitive)) path += ".ifcfed";
-
- QString err;
- if (!federation_->save(path, &err)) {
- QMessageBox::warning(this, "Save Federation",
- QString("Could not save federation:\n%1").arg(err));
- return false;
- }
- updateWindowTitle();
- return true;
-}
-
-void MainWindow::onSetHomeView() {
- auto cs = viewport_->cameraState();
- Federation::HomeView hv;
- hv.target = cs.target;
- hv.distance = cs.distance;
- hv.yaw = cs.yaw;
- hv.pitch = cs.pitch;
- federation_->setHomeView(hv);
- updateWindowTitle();
-}
-
-void MainWindow::onGoHomeView() {
- if (!federation_->hasHomeView()) {
- status_label_->setText("No home view set for this federation.");
- return;
- }
- const auto& hv = federation_->homeView();
- viewport_->setCamera(hv.target.x(), hv.target.y(), hv.target.z(),
- hv.distance, hv.yaw, hv.pitch);
-}
-
-void MainWindow::clearScene() {
- while (!tree_roots_.empty()) {
- uint32_t mid = tree_roots_.begin()->first;
- viewport_->removeModel(mid);
- loader_->removeModel(mid);
- removeModelUi(mid);
- }
- fed_id_to_model_id_.clear();
- model_id_to_fed_id_.clear();
- for (auto& kv : group_tree_items_) delete kv.second;
- group_tree_items_.clear();
-}
-
-bool MainWindow::confirmDiscardIfDirty() {
- if (!federation_->isDirty()) return true;
- auto ret = QMessageBox::question(
- this, "Unsaved Federation",
- "The current federation has unsaved changes. Save before continuing?",
- QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel,
- QMessageBox::Save);
- if (ret == QMessageBox::Cancel) return false;
- if (ret == QMessageBox::Save) return onFederationSave();
- return true; // Discard
}
void MainWindow::updateWindowTitle() {
- QString fed_path = federation_->filePath();
- if (fed_path.isEmpty() && federation_->models().empty()) {
- setWindowTitle("IfcViewer");
- } else if (fed_path.isEmpty()) {
- setWindowTitle("untitled[*] — IfcViewer");
+ const QString project_path = federation_ ? federation_->filePath() : QString();
+ if (project_path.isEmpty() && (!federation_ || federation_->models().empty())) {
+ setWindowTitle("IfcOpenShell Interface");
+ } else if (project_path.isEmpty()) {
+ setWindowTitle("untitled[*] - IfcOpenShell Interface");
} else {
- setWindowTitle(QFileInfo(fed_path).fileName() + "[*] — IfcViewer");
- }
- setWindowModified(federation_->isDirty());
-}
-
-void MainWindow::closeEvent(QCloseEvent* event) {
- if (confirmDiscardIfDirty()) event->accept();
- else event->ignore();
-}
-
-void MainWindow::onLoadStarted(uint32_t /*mid*/, QString display_name) {
- progress_bar_->setValue(0);
- progress_bar_->setVisible(true);
- status_label_->setText("Loading: " + display_name);
-}
-
-void MainWindow::onLoadProgressChanged(int percent) {
- progress_bar_->setValue(percent);
-}
-
-void MainWindow::appendElementToTree(uint32_t model_id,
- uint32_t object_id,
- int ifc_id,
- int parent_ifc_id,
- const std::string& guid,
- const std::string& name,
- const std::string& type) {
- auto root_it = tree_roots_.find(model_id);
- QTreeWidgetItem* parent_item = (root_it != tree_roots_.end()) ? root_it->second : nullptr;
-
- auto parent_obj_it = scoped_ifc_id_to_object_id_.find(
- scopedKey(model_id, parent_ifc_id));
- if (parent_obj_it != scoped_ifc_id_to_object_id_.end()) {
- auto tree_it = tree_items_.find(parent_obj_it->second);
- if (tree_it != tree_items_.end()) {
- parent_item = tree_it->second;
- }
- }
-
- QString display_name = QString::fromStdString(name);
- if (display_name.isEmpty()) {
- display_name = QString::fromStdString(type) + " #" + QString::number(ifc_id);
- }
-
- auto* item = new QTreeWidgetItem(parent_item);
- item->setText(0, display_name);
- item->setText(1, QString::fromStdString(type));
- item->setText(2, QString::fromStdString(guid));
- item->setData(0, Qt::UserRole, object_id);
-
- tree_items_[object_id] = item;
-}
-
-void MainWindow::onSidecarElementsReady(uint32_t mid,
- std::vector elements,
- std::string string_table) {
- auto str = [&](uint32_t offset, uint32_t length) -> std::string {
- if (length == 0 || offset + length > string_table.size()) return {};
- return string_table.substr(offset, length);
- };
-
- QElapsedTimer t;
- t.start();
- element_tree_->setUpdatesEnabled(false);
-
- for (const auto& pe : elements) {
- ElementInfo info;
- info.object_id = pe.object_id;
- info.model_id = pe.model_id;
- info.ifc_id = pe.ifc_id;
- info.parent_id = pe.parent_id;
- info.guid = str(pe.guid_offset, pe.guid_length);
- info.name = str(pe.name_offset, pe.name_length);
- info.type = str(pe.type_offset, pe.type_length);
-
- element_map_[info.object_id] = info;
- scoped_ifc_id_to_object_id_[scopedKey(info.model_id, info.ifc_id)] = info.object_id;
-
- appendElementToTree(info.model_id, info.object_id, info.ifc_id,
- info.parent_id, info.guid, info.name, info.type);
- }
-
- element_tree_->setUpdatesEnabled(true);
- qDebug(" Tree build: %lld ms (%zu elements)", t.elapsed(), elements.size());
-}
-
-void MainWindow::onDataSourceReady(uint32_t mid) {
- // The IFC file is now available — push the model's CoordinateOperation
- // (or identity) to the viewport. Sidecar-hit models get here for the
- // first time; stream-loaded models also pass through here when a
- // separate data source opens, but applyCoordinateOperationToViewport
- // is idempotent so double-applying is harmless.
- applyCoordinateOperationToViewport(mid);
-
- // Re-populate if the current selection belongs to this model, since
- // populateProperties() now has an ifcFile() to query.
- auto items = element_tree_->selectedItems();
- if (items.isEmpty()) return;
- uint32_t object_id = items.first()->data(0, Qt::UserRole).toUInt();
- auto it = element_map_.find(object_id);
- if (it != element_map_.end() && it->second.model_id == mid) {
- populateProperties(object_id);
+ setWindowTitle(QFileInfo(project_path).fileName() + "[*] - IfcOpenShell Interface");
}
}
-void MainWindow::applyCoordinateOperationToViewport(uint32_t mid) {
- Eigen::Matrix4d M = Eigen::Matrix4d::Identity();
- if (const ModelGeoref* gr = loader_->modelGeoref(mid)) {
- if (gr->has_coordinate_operation) {
- M = gr->coordinate_operation_meters;
- }
- }
- viewport_->setModelCoordinateOperation(mid, M);
- // ModelTransformation's compose can depend on the active
- // CoordinateOperation (when ModelTransformation.a_frame == ModelLocal,
- // a is lifted through stage 2), so re-push it whenever stage 2 changes.
- applyModelTransformationToViewport(mid);
-}
-
-void MainWindow::applyModelTransformationToViewport(uint32_t mid) {
- Eigen::Matrix4d M = Eigen::Matrix4d::Identity();
- auto fed_it = model_id_to_fed_id_.find(mid);
- if (fed_it != model_id_to_fed_id_.end()) {
- if (const Federation::Model* m = federation_->findById(fed_it->second)) {
- // ModelUnits + the active CoordinateOperation come from the
- // ModelGeoref cache; defaults are safe (1.0, 1.0, identity)
- // when the IFC file isn't yet available.
- ModelUnits units;
- Eigen::Matrix4d coord_op = Eigen::Matrix4d::Identity();
- if (const ModelGeoref* gr = loader_->modelGeoref(mid)) {
- units = gr->units;
- if (gr->has_coordinate_operation) {
- coord_op = gr->coordinate_operation_meters;
- }
- }
- M = composeModelTransformation(
- m->model_transformation, federation_->config(),
- units, coord_op);
- }
- }
- viewport_->setModelTransformation(mid, M);
-}
-
-void MainWindow::applyFederatedFalseOriginToViewport() {
- const Eigen::Matrix4d M = composeFederatedFalseOrigin(
- federation_->federatedFalseOrigin(), federation_->config());
- viewport_->setFederatedFalseOrigin(M);
-}
-
-void MainWindow::maybeGuessFederatedFalseOrigin(uint32_t mid) {
- // Only auto-guess for untitled federations. A saved .ifcfed carries its
- // authoritative origin (even if that happens to be the default), so we
- // never silently overwrite it when the user re-adds a model.
- if (!federation_->filePath().isEmpty()) return;
-
- // Skip once the origin is non-default — either the user edited it, a
- // previous batch already guessed, or a load-from-file populated it.
- // For a multi-file batch this means whichever model finishes first
- // anchors the federation; the rest see a non-default origin and skip.
- const FederatedFalseOrigin& cur = federation_->federatedFalseOrigin();
- const FederatedFalseOrigin def;
- if (cur.xyz != def.xyz || cur.rz_deg != def.rz_deg) return;
-
- const Eigen::Matrix4d* placement = loader_->firstPlacement(mid);
- const ModelGeoref* gr = loader_->modelGeoref(mid);
- if (placement == nullptr || gr == nullptr) return;
-
- const FederatedFalseOrigin guess = guessFederatedFalseOrigin(
- *placement, *gr, federation_->config());
- federation_->setFederatedFalseOrigin(guess);
-}
-
-void MainWindow::onLoadedFromSidecar(uint32_t mid, qint64 elapsed_ms) {
- progress_bar_->setVisible(false);
- status_label_->setText(QString("%1 elements across %2 model(s) — loaded from cache in %3")
- .arg(element_map_.size())
- .arg(loader_->modelCount())
- .arg(formatElapsed(elapsed_ms)));
-
- // Sidecar v11+ caches the CoordinateOperation, so SceneLoader has
- // already populated modelGeoref by now — push CoordinateOperation +
- // ModelTransformation immediately rather than waiting for the
- // (possibly never-arriving) data-source load.
- applyCoordinateOperationToViewport(mid);
- applyModelVisibilityToViewport(mid);
- maybeGuessFederatedFalseOrigin(mid);
-}
-
-void MainWindow::onStreamedElementsReady(uint32_t /*mid*/, std::vector elements) {
- for (const auto& info : elements) {
- element_map_[info.object_id] = info;
- scoped_ifc_id_to_object_id_[scopedKey(info.model_id, info.ifc_id)] = info.object_id;
- appendElementToTree(info.model_id, info.object_id, info.ifc_id,
- info.parent_id, info.guid, info.name, info.type);
- }
-}
-
-void MainWindow::writeSidecarForModel(uint32_t mid) {
- SidecarData sd;
- if (!viewport_->snapshotModel(mid, sd)) return;
-
- // Cache the model's CoordinateOperation alongside the geometry so a
- // sidecar load doesn't need the IFC source just to apply georef.
- if (const ModelGeoref* gr = loader_->modelGeoref(mid)) {
- sd.has_coordinate_operation = gr->has_coordinate_operation ? 1 : 0;
- Eigen::Map>(
- sd.coordinate_operation_meters) = gr->coordinate_operation_meters;
- sd.project_length_to_meters = gr->units.project_length_to_meters;
- sd.map_unit_to_meters = gr->units.map_unit_to_meters;
- }
-
- for (const auto& [oid, info] : element_map_) {
- if (info.model_id != mid) continue;
- PackedElementInfo pe;
- pe.object_id = info.object_id;
- pe.model_id = info.model_id;
- pe.ifc_id = info.ifc_id;
- pe.parent_id = info.parent_id;
- pe.guid_offset = static_cast(sd.string_table.size());
- pe.guid_length = static_cast(info.guid.size());
- sd.string_table += info.guid;
- pe.name_offset = static_cast(sd.string_table.size());
- pe.name_length = static_cast(info.name.size());
- sd.string_table += info.name;
- pe.type_offset = static_cast(sd.string_table.size());
- pe.type_length = static_cast(info.type.size());
- sd.string_table += info.type;
- sd.elements.push_back(pe);
- }
-
- QElapsedTimer t_lod; t_lod.start();
- buildLods(sd);
- LodStats ls = summariseLods(sd);
- qDebug(" LOD build: %lld ms — %u/%u meshes got LOD1 "
- "(%u tris -> %u tris for those meshes)",
- t_lod.elapsed(),
- ls.meshes_with_lod1, ls.meshes_total,
- ls.tris_lod0_for_lod1, ls.tris_lod1);
- viewport_->applyLodExtension(mid, sd);
-
- QElapsedTimer t; t.start();
- bool ok = writeSidecar(loader_->filePath(mid).toStdString(), sd);
- qDebug(" Sidecar write: %lld ms (%s)", t.elapsed(), ok ? "ok" : "FAILED");
-}
-
-void MainWindow::removeModelUi(uint32_t mid) {
- auto fed_it = model_id_to_fed_id_.find(mid);
- if (fed_it != model_id_to_fed_id_.end()) {
- fed_id_to_model_id_.erase(fed_it->second);
- model_id_to_fed_id_.erase(fed_it);
- }
-
- auto root_it = tree_roots_.find(mid);
- if (root_it != tree_roots_.end()) {
- delete root_it->second;
- tree_roots_.erase(root_it);
- }
-
- for (auto it = tree_items_.begin(); it != tree_items_.end();) {
- auto info_it = element_map_.find(it->first);
- if (info_it != element_map_.end() && info_it->second.model_id == mid) {
- it = tree_items_.erase(it);
- } else {
- ++it;
- }
- }
-
- for (auto it = element_map_.begin(); it != element_map_.end();) {
- if (it->second.model_id == mid) {
- it = element_map_.erase(it);
- } else {
- ++it;
- }
- }
-
- for (auto it = scoped_ifc_id_to_object_id_.begin(); it != scoped_ifc_id_to_object_id_.end();) {
- if (static_cast(it->first >> 32) == mid) {
- it = scoped_ifc_id_to_object_id_.erase(it);
- } else {
- ++it;
- }
- }
-
- viewport_->setSelectedObjectId(0);
- property_table_->setRowCount(0);
-}
-
-void MainWindow::onLoadedFromStream(uint32_t mid, qint64 elapsed_ms) {
- progress_bar_->setVisible(false);
- status_label_->setText(QString("%1 elements across %2 model(s) — last loaded in %3")
- .arg(element_map_.size())
- .arg(loader_->modelCount())
- .arg(formatElapsed(elapsed_ms)));
-
- applyCoordinateOperationToViewport(mid);
- applyModelVisibilityToViewport(mid);
- maybeGuessFederatedFalseOrigin(mid);
-
- writeSidecarForModel(mid);
-}
-
-void MainWindow::onLoadCancelled(uint32_t mid) {
- progress_bar_->setVisible(false);
- removeModelUi(mid);
- status_label_->setText(QString("%1 load cancelled").arg(loader_->displayName(mid)));
-}
-
-void MainWindow::onLoadError(uint32_t mid, QString message) {
- progress_bar_->setVisible(false);
- removeModelUi(mid);
- status_label_->setText("Error: " + message);
- QMessageBox::warning(this, "Error", message);
-}
-
-void MainWindow::onAllLoadsFinished() {
- applyPendingBenchmark();
-}
-
-void MainWindow::onObjectPicked(uint32_t object_id) {
- // Signal originates from the viewport's SelectionState — viewport
- // selection is already authoritative. Pushing setSelectedObjectId
- // back here would clobber a multi-select set with {object_id}.
- auto it = tree_items_.find(object_id);
- if (it != tree_items_.end()) {
- element_tree_->blockSignals(true);
- element_tree_->setCurrentItem(it->second);
- element_tree_->blockSignals(false);
- } else {
- // No active (e.g. selection cleared, or active id is in a model
- // whose tree node hasn't materialised) — drop the tree highlight
- // so the panel doesn't lie about what's active.
- element_tree_->blockSignals(true);
- element_tree_->setCurrentItem(nullptr);
- element_tree_->blockSignals(false);
- }
-
- populateProperties(object_id);
- updateVolumeReadout();
-}
-
-void MainWindow::updateVolumeReadout() {
- // Volume readout only renders while the volume tool is active —
- // matches Area/Length, which are also gated behind their own tool
- // mode. Other modes own the HUD + overlay labels for their
- // lifetime, so we stay quiet here.
- if (viewport_->toolMode() != ViewportWindow::ToolMode::Volume) return;
-
- const auto& sel = viewport_->selection().selectionIds();
- if (sel.empty()) {
- viewport_->setHudText(QString());
- viewport_->setOverlayLabels({});
- return;
- }
-
- std::vector ids(sel.begin(), sel.end());
- const auto per_obj = volumesPerObject(*viewport_, ids);
-
- double total = 0.0;
- std::vector labels;
- labels.reserve(per_obj.size());
- for (const auto& [oid, v] : per_obj) {
- total += v;
- QVector3D mn, mx;
- if (!viewport_->computeObjectAabb(oid, mn, mx)) continue;
- OverlayRenderer::Label lbl;
- const QVector3D c = (mn + mx) * 0.5f;
- lbl.world_pos[0] = c.x();
- lbl.world_pos[1] = c.y();
- lbl.world_pos[2] = c.z();
- lbl.text = QString::number(v, 'f', 4) + " m³";
- labels.push_back(std::move(lbl));
- }
-
- viewport_->setHudText(QString("Volume: %1 m³ (%2 object%3)")
- .arg(total, 0, 'f', 4)
- .arg(per_obj.size())
- .arg(per_obj.size() == 1 ? "" : "s"));
- viewport_->setOverlayLabels(labels);
-}
-
-void MainWindow::onTreeSelectionChanged() {
- auto items = element_tree_->selectedItems();
- if (items.isEmpty()) return;
-
- uint32_t object_id = items.first()->data(0, Qt::UserRole).toUInt();
- // Tree stays single-select per UX choice — clicking a tree item
- // replaces the viewport's multi-set with just that item.
- viewport_->setSelectedObjectId(object_id);
- populateProperties(object_id);
-}
-
-void MainWindow::populateProperties(uint32_t object_id) {
- property_table_->setRowCount(0);
- if (object_id == 0) return;
-
- auto it = element_map_.find(object_id);
- if (it == element_map_.end()) return;
-
- const auto& info = it->second;
-
- auto addRow = [this](const QString& key, const QString& value) {
- int row = property_table_->rowCount();
- property_table_->insertRow(row);
- property_table_->setItem(row, 0, new QTableWidgetItem(key));
- property_table_->setItem(row, 1, new QTableWidgetItem(value));
- };
-
- addRow("IFC ID", QString::number(info.ifc_id));
- addRow("GUID", QString::fromStdString(info.guid));
- addRow("Name", QString::fromStdString(info.name));
- addRow("Type", QString::fromStdString(info.type));
-
- auto* file = loader_->ifcFile(info.model_id);
- if (!file) return;
-
- auto product = file->instance_by_id(info.ifc_id);
- if (!product) return;
-
- auto& decl = product.declaration();
- if (auto* entity = decl.as_entity()) {
- for (size_t i = 0; i < entity->attribute_count(); ++i) {
- auto* attr = entity->attribute_by_index(i);
- try {
- auto val = product.get_attribute_value(i);
- if (!val.isNull()) {
- std::string str_val;
- try {
- str_val = static_cast(val);
- } catch (...) {
- str_val = "<" + std::string(ifcopenshell::argument_type_to_string(val.type())) + ">";
- }
- addRow(QString::fromStdString(attr->name()), QString::fromStdString(str_val));
- }
- } catch (...) {}
- }
- }
-}
-
-void MainWindow::setPendingCamera(const QString& params) {
- pending_camera_ = params;
-}
-
-void MainWindow::setPendingBenchmark(int frames) {
- pending_benchmark_ = frames;
-}
-
-void MainWindow::applyPendingBenchmark() {
- if (pending_camera_.isEmpty() && pending_benchmark_ <= 0) return;
-
- if (!pending_camera_.isEmpty()) {
- QStringList parts = pending_camera_.split(',');
- if (parts.size() == 6) {
- viewport_->setCamera(
- parts[0].toFloat(), parts[1].toFloat(), parts[2].toFloat(),
- parts[3].toFloat(), parts[4].toFloat(), parts[5].toFloat());
- qDebug("Camera set: %s", qPrintable(pending_camera_));
- } else {
- qWarning("--camera expects 6 comma-separated values: tx,ty,tz,dist,yaw,pitch");
- }
- pending_camera_.clear();
- }
-
- if (pending_benchmark_ > 0) {
- qDebug("Starting benchmark: %d frames", pending_benchmark_);
- viewport_->setBenchmarkFrames(pending_benchmark_);
- pending_benchmark_ = 0;
- }
-}
-
-QString MainWindow::formatElapsed(qint64 ms) const {
- return (ms >= 1000)
- ? QString::number(ms / 1000.0, 'f', 2) + " s"
- : QString::number(ms) + " ms";
-}
-
-uint32_t MainWindow::modelIdForRoot(QTreeWidgetItem* item) const {
- if (!item) return 0;
- for (const auto& kv : tree_roots_) {
- if (kv.second == item) return kv.first;
- }
- return 0;
-}
-
-void MainWindow::applyModelVisibilityToViewport(uint32_t mid) {
- auto fed_it = model_id_to_fed_id_.find(mid);
- if (fed_it == model_id_to_fed_id_.end()) return;
- const QString& fed_id = fed_it->second;
- const bool effective = federation_->isModelEffectivelyVisible(fed_id);
- if (effective) viewport_->showModel(mid);
- else viewport_->hideModel(mid);
-
- // Tree-side cue: italicise + grey out the model root when not
- // effectively visible (own toggle off, or any ancestor group hidden).
- auto root_it = tree_roots_.find(mid);
- if (root_it != tree_roots_.end()) {
- QFont f = root_it->second->font(0);
- f.setItalic(!effective);
- for (int col = 0; col < element_tree_->columnCount(); ++col) {
- root_it->second->setFont(col, f);
- root_it->second->setForeground(
- col,
- effective ? element_tree_->palette().color(QPalette::Text)
- : element_tree_->palette().color(QPalette::Disabled,
- QPalette::Text));
- }
- }
-}
-
-void MainWindow::onTreeContextMenu(const QPoint& pos) {
- QTreeWidgetItem* item = element_tree_->itemAt(pos);
- const uint32_t mid = modelIdForRoot(item);
- const QString group_id = groupIdForItem(item);
- // Element rows (children of a model root) are excluded — only model
- // roots, group rows, and the empty area get a menu.
- const bool is_element_row =
- item != nullptr && mid == 0 && group_id.isEmpty();
- if (is_element_row) return;
-
- QMenu menu(this);
-
- if (mid != 0) {
- // === Model row ===
- auto fed_it = model_id_to_fed_id_.find(mid);
- if (fed_it == model_id_to_fed_id_.end()) return;
- const Federation::Model* m = federation_->findById(fed_it->second);
- if (!m) return;
-
- const bool currently_loading = loader_->isLoadingModel(mid);
-
- QAction* hide_show = menu.addAction(m->visible ? "Hide" : "Show");
-
- QMenu* move_menu = menu.addMenu("Move to Group");
- QAction* move_to_root = move_menu->addAction("(Root)");
- move_to_root->setEnabled(!m->group_id.isEmpty());
- move_menu->addSeparator();
- std::vector> move_targets;
- for (const Federation::Group* g : federation_->allGroups()) {
- QAction* a = move_menu->addAction(g->display_name);
- a->setEnabled(g->id != m->group_id);
- move_targets.emplace_back(a, g->id);
- }
- if (federation_->allGroups().empty()) {
- QAction* none = move_menu->addAction("(no groups)");
- none->setEnabled(false);
- }
-
- QAction* remove = menu.addAction("Remove");
- remove->setEnabled(!currently_loading);
-
- QAction* chosen = menu.exec(element_tree_->viewport()->mapToGlobal(pos));
- if (!chosen) return;
- if (chosen == hide_show) {
- federation_->setModelVisible(fed_it->second, !m->visible);
- } else if (chosen == move_to_root) {
- federation_->setModelGroup(fed_it->second, QString());
- } else if (chosen == remove) {
- removeModel(mid);
- } else {
- for (const auto& [a, gid] : move_targets) {
- if (chosen == a) {
- federation_->setModelGroup(fed_it->second, gid);
- break;
- }
- }
- }
- return;
- }
-
- if (!group_id.isEmpty()) {
- // === Group row ===
- const Federation::Group* g = federation_->findGroupById(group_id);
- if (!g) return;
-
- QAction* hide_show = menu.addAction(g->visible ? "Hide" : "Show");
- QAction* rename = menu.addAction("Rename...");
- QAction* new_sub = menu.addAction("New Subgroup");
-
- QMenu* move_menu = menu.addMenu("Move to Parent");
- QAction* move_to_root = move_menu->addAction("(Root)");
- move_to_root->setEnabled(g->parent != nullptr);
- move_menu->addSeparator();
- std::vector> move_targets;
- for (const Federation::Group* og : federation_->allGroups()) {
- QAction* a = move_menu->addAction(og->display_name);
- // Disable self, current parent, and any descendant of g (the
- // latter would create a cycle). Walk og's ancestor chain to
- // detect descendants.
- bool would_cycle = false;
- for (const Federation::Group* cur = og; cur; cur = cur->parent) {
- if (cur == g) { would_cycle = true; break; }
- }
- const bool is_current_parent =
- g->parent != nullptr && og == g->parent;
- a->setEnabled(!would_cycle && !is_current_parent);
- move_targets.emplace_back(a, og->id);
- }
-
- QAction* remove = menu.addAction("Remove Group");
-
- QAction* chosen = menu.exec(element_tree_->viewport()->mapToGlobal(pos));
- if (!chosen) return;
- if (chosen == hide_show) {
- federation_->setGroupVisible(group_id, !g->visible);
- } else if (chosen == rename) {
- bool ok = false;
- QString name = QInputDialog::getText(
- this, "Rename Group", "Group name:", QLineEdit::Normal,
- g->display_name, &ok);
- if (ok && !name.isEmpty()) federation_->setGroupName(group_id, name);
- } else if (chosen == new_sub) {
- bool ok = false;
- QString name = QInputDialog::getText(
- this, "New Subgroup", "Group name:", QLineEdit::Normal,
- "Group", &ok);
- if (ok && !name.isEmpty()) federation_->addGroup(name, group_id);
- } else if (chosen == move_to_root) {
- federation_->setGroupParent(group_id, QString());
- } else if (chosen == remove) {
- federation_->removeGroup(group_id);
- } else {
- for (const auto& [a, gid] : move_targets) {
- if (chosen == a) {
- federation_->setGroupParent(group_id, gid);
- break;
- }
- }
- }
- return;
- }
-
- // === Empty area ===
- QAction* new_group = menu.addAction("New Group");
- QAction* chosen = menu.exec(element_tree_->viewport()->mapToGlobal(pos));
- if (!chosen) return;
- if (chosen == new_group) {
- bool ok = false;
- QString name = QInputDialog::getText(
- this, "New Group", "Group name:", QLineEdit::Normal,
- "Group", &ok);
- if (ok && !name.isEmpty()) federation_->addGroup(name, QString());
- }
-}
-
-void MainWindow::removeModel(uint32_t mid) {
- if (loader_->isLoadingModel(mid)) return;
-
- QString fed_id;
- auto fed_it = model_id_to_fed_id_.find(mid);
- if (fed_it != model_id_to_fed_id_.end()) fed_id = fed_it->second;
-
- viewport_->removeModel(mid);
- loader_->removeModel(mid);
- removeModelUi(mid);
- if (!fed_id.isEmpty()) federation_->removeModel(fed_id);
- updateWindowTitle();
-}
-
-QString MainWindow::groupIdForItem(QTreeWidgetItem* item) const {
- if (!item) return {};
- for (const auto& kv : group_tree_items_) {
- if (kv.second == item) return kv.first;
- }
- return {};
-}
-
-QTreeWidgetItem* MainWindow::ensureGroupTreeItem(const QString& group_id) {
- if (group_id.isEmpty()) return nullptr;
- auto it = group_tree_items_.find(group_id);
- if (it != group_tree_items_.end()) return it->second;
- const Federation::Group* g = federation_->findGroupById(group_id);
- if (!g) return nullptr;
-
- auto* item = new QTreeWidgetItem();
- item->setText(0, g->display_name);
- item->setText(1, "Group");
- group_tree_items_[group_id] = item;
- return item;
-}
-
-void MainWindow::reparentGroupTreeItem(const QString& group_id) {
- auto it = group_tree_items_.find(group_id);
- if (it == group_tree_items_.end()) return;
- QTreeWidgetItem* item = it->second;
- const Federation::Group* g = federation_->findGroupById(group_id);
- if (!g) return;
-
- QTreeWidgetItem* desired_parent = nullptr;
- if (g->parent != nullptr) {
- auto pit = group_tree_items_.find(g->parent->id);
- if (pit != group_tree_items_.end()) desired_parent = pit->second;
- }
-
- QTreeWidgetItem* current_parent = item->parent();
- if (current_parent == desired_parent &&
- (current_parent != nullptr ||
- element_tree_->indexOfTopLevelItem(item) >= 0)) {
- return;
- }
-
- // Detach from current location.
- if (current_parent) {
- current_parent->removeChild(item);
- } else {
- int idx = element_tree_->indexOfTopLevelItem(item);
- if (idx >= 0) element_tree_->takeTopLevelItem(idx);
- }
- // Attach to desired location.
- if (desired_parent) desired_parent->addChild(item);
- else element_tree_->addTopLevelItem(item);
-}
-
-void MainWindow::reparentModelTreeRoot(uint32_t mid) {
- auto root_it = tree_roots_.find(mid);
- if (root_it == tree_roots_.end()) return;
- QTreeWidgetItem* item = root_it->second;
-
- auto fed_it = model_id_to_fed_id_.find(mid);
- QString group_id;
- if (fed_it != model_id_to_fed_id_.end()) {
- if (const Federation::Model* m = federation_->findById(fed_it->second)) {
- group_id = m->group_id;
- }
- }
-
- QTreeWidgetItem* desired_parent = nullptr;
- if (!group_id.isEmpty()) {
- auto pit = group_tree_items_.find(group_id);
- if (pit != group_tree_items_.end()) desired_parent = pit->second;
- }
-
- QTreeWidgetItem* current_parent = item->parent();
- if (current_parent == desired_parent &&
- (current_parent != nullptr ||
- element_tree_->indexOfTopLevelItem(item) >= 0)) {
- return;
- }
-
- if (current_parent) {
- current_parent->removeChild(item);
- } else {
- int idx = element_tree_->indexOfTopLevelItem(item);
- if (idx >= 0) element_tree_->takeTopLevelItem(idx);
- }
- if (desired_parent) desired_parent->addChild(item);
- else element_tree_->addTopLevelItem(item);
-}
-
-void MainWindow::refreshGroupRowAppearance(const QString& group_id) {
- auto it = group_tree_items_.find(group_id);
- if (it == group_tree_items_.end()) return;
- QTreeWidgetItem* item = it->second;
- const bool effective = federation_->isGroupChainVisible(group_id);
-
- QFont f = item->font(0);
- f.setItalic(!effective);
- f.setBold(true);
- for (int col = 0; col < element_tree_->columnCount(); ++col) {
- item->setFont(col, f);
- item->setForeground(
- col,
- effective ? element_tree_->palette().color(QPalette::Text)
- : element_tree_->palette().color(QPalette::Disabled,
- QPalette::Text));
- }
-}
-
-std::vector MainWindow::descendantGroupIds(const QString& group_id) const {
- std::vector out;
- auto walk = [&](auto&& self,
- const std::vector>& src) -> void {
- for (const auto& g : src) {
- out.push_back(g->id);
- self(self, g->children);
- }
- };
- if (group_id.isEmpty()) {
- walk(walk, federation_->rootGroups());
- } else {
- const Federation::Group* g = federation_->findGroupById(group_id);
- if (!g) return out;
- out.push_back(g->id);
- walk(walk, g->children);
- }
- return out;
-}
-
-void MainWindow::applyVisibilityCascadeFromGroup(const QString& group_id) {
- const std::vector gids = descendantGroupIds(group_id);
- // Models directly assigned to one of these groups need their viewport
- // visibility re-pushed because chain visibility may have flipped.
- for (const auto& m : federation_->models()) {
- const bool affected =
- (group_id.isEmpty()) ||
- std::find(gids.begin(), gids.end(), m.group_id) != gids.end();
- if (!affected) continue;
- auto it = fed_id_to_model_id_.find(m.id);
- if (it != fed_id_to_model_id_.end()) {
- applyModelVisibilityToViewport(it->second);
- }
- }
-}
+} // namespace ifcinterface::shell
diff --git a/src/ifcviewer-full/MainWindow.h b/src/ifcviewer-full/MainWindow.h
index 2d26cda6fd..1439d8bf91 100644
--- a/src/ifcviewer-full/MainWindow.h
+++ b/src/ifcviewer-full/MainWindow.h
@@ -1,3 +1,4 @@
+// This file was generated with the assistance of an AI coding tool.
/********************************************************************************
* *
* This file is part of IfcOpenShell. *
@@ -17,195 +18,85 @@
* *
********************************************************************************/
-#ifndef MAINWINDOW_H
-#define MAINWINDOW_H
+#ifndef IFCINTERFACE_SHELL_MAINWINDOW_H
+#define IFCINTERFACE_SHELL_MAINWINDOW_H
+#include
#include
-#include
-#include
-#include
-#include
-#include
-#include
-
-#include