From 742561c56dde50d1fffa130ac4701632daf0759d Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 10 May 2026 22:04:03 +1000 Subject: [PATCH] Interface mockup 14 --- src/interface/components/Dialog.cpp | 43 +- src/interface/components/Dialog.h | 2 + src/interface/components/Panel.cpp | 4 +- src/interface/components/Section.cpp | 17 + src/interface/components/Section.h | 2 + src/interface/components/Style.cpp | 16 +- src/interface/components/Style.h | 6 + src/interface/main.cpp | 10 +- src/interface/modules/models/Controller.cpp | 2 +- src/interface/modules/models/Panel.cpp | 4 +- .../modules/models/SettingsDialog.cpp | 525 +++++++++++------- src/interface/modules/models/SettingsDialog.h | 62 ++- src/interface/modules/settings/Dialog.cpp | 101 +++- src/interface/modules/settings/Dialog.h | 7 + 14 files changed, 520 insertions(+), 281 deletions(-) diff --git a/src/interface/components/Dialog.cpp b/src/interface/components/Dialog.cpp index 410e3a581b..9f1c9cdde5 100644 --- a/src/interface/components/Dialog.cpp +++ b/src/interface/components/Dialog.cpp @@ -48,26 +48,29 @@ Dialog::Dialog(QWidget* parent, bool scrollable) style::metrics::section_body_padding); frame_layout->setSpacing(0); - if (scrollable) { - auto* scroll = new QScrollArea(frame); - scroll->setWidgetResizable(true); - scroll->setFrameShape(QFrame::NoFrame); + auto* scroll = new QScrollArea(frame); + scroll->setWidgetResizable(true); + scroll->setFrameShape(QFrame::NoFrame); - auto* scroll_body = new QWidget(scroll); - scroll_body->setObjectName("panelScrollBody"); - body_layout_ = new QVBoxLayout(scroll_body); - body_layout_->setContentsMargins(0, 0, 0, 0); - body_layout_->setSpacing(style::metrics::section_body_padding); + auto* scroll_body = new QWidget(scroll); + scroll_body->setObjectName("panelScrollBody"); + body_layout_ = new QVBoxLayout(scroll_body); + body_layout_->setContentsMargins(0, 0, 0, 0); + body_layout_->setSpacing(style::metrics::section_body_padding); + body_layout_->setAlignment(Qt::AlignTop); - scroll->setWidget(scroll_body); - frame_layout->addWidget(scroll); - } else { - auto* body = new QWidget(frame); - body_layout_ = new QVBoxLayout(body); - body_layout_->setContentsMargins(0, 0, 0, 0); - body_layout_->setSpacing(style::metrics::section_body_padding); - frame_layout->addWidget(body); - } + scroll->setWidget(scroll_body); + frame_layout->addWidget(scroll, scrollable ? 1 : 0); + + auto* footer = new QWidget(frame); + footer_layout_ = new QVBoxLayout(footer); + footer_layout_->setContentsMargins(style::metrics::section_body_padding, + style::metrics::section_body_padding, + style::metrics::section_body_padding, + 0); + footer_layout_->setSpacing(style::metrics::section_body_padding); + footer_layout_->setAlignment(Qt::AlignTop); + frame_layout->addWidget(footer); outer_layout->addWidget(frame); } @@ -76,4 +79,8 @@ void Dialog::addBodyWidget(QWidget* widget) { body_layout_->addWidget(widget); } +void Dialog::addFooterWidget(QWidget* widget) { + footer_layout_->addWidget(widget); +} + } // namespace ifcinterface::components diff --git a/src/interface/components/Dialog.h b/src/interface/components/Dialog.h index 79a119adda..9145cce428 100644 --- a/src/interface/components/Dialog.h +++ b/src/interface/components/Dialog.h @@ -36,9 +36,11 @@ public: bool scrollable = false); void addBodyWidget(QWidget* widget); + void addFooterWidget(QWidget* widget); private: QVBoxLayout* body_layout_ = nullptr; + QVBoxLayout* footer_layout_ = nullptr; }; } // namespace ifcinterface::components diff --git a/src/interface/components/Panel.cpp b/src/interface/components/Panel.cpp index b7cdccab2e..99a0531636 100644 --- a/src/interface/components/Panel.cpp +++ b/src/interface/components/Panel.cpp @@ -98,6 +98,7 @@ Panel::Panel(const QString& title, QWidget* content, QWidget* parent, bool has_s body_layout_ = new QVBoxLayout(scroll_body); body_layout_->setContentsMargins(0, 0, 0, 0); body_layout_->setSpacing(style::metrics::section_body_padding); + body_layout_->setAlignment(Qt::AlignTop); scroll->setWidget(scroll_body); frame_layout->addWidget(scroll); @@ -106,11 +107,12 @@ Panel::Panel(const QString& title, QWidget* content, QWidget* parent, bool has_s body_layout_ = new QVBoxLayout(body); body_layout_->setContentsMargins(0, 0, 0, 0); body_layout_->setSpacing(style::metrics::section_body_padding); + body_layout_->setAlignment(Qt::AlignTop); frame_layout->addWidget(body); } if (content) { - body_layout_->addWidget(content); + addBodyWidget(content); } outer_layout->addWidget(frame); diff --git a/src/interface/components/Section.cpp b/src/interface/components/Section.cpp index 2837dee1dc..79e8e5f62a 100644 --- a/src/interface/components/Section.cpp +++ b/src/interface/components/Section.cpp @@ -24,6 +24,7 @@ #include #include +#include #include #include @@ -33,6 +34,7 @@ Section::Section(const QString& title, SectionHeaderMode header_mode, QWidget* p : QWidget(parent) { setObjectName("panelSection"); + setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum); auto* layout = new QVBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(style::metrics::padding); @@ -59,6 +61,11 @@ Section::Section(const QString& title, SectionHeaderMode header_mode, QWidget* p connect(toggle_button_, &QToolButton::toggled, this, [this](bool expanded) { toggle_button_->setArrowType(expanded ? Qt::DownArrow : Qt::RightArrow); body_->setVisible(expanded); + body_->setMaximumHeight(expanded ? QWIDGETSIZE_MAX : 0); + body_->setMinimumHeight(0); + setSizePolicy(QSizePolicy::Preferred, + body_expanding_ && expanded ? QSizePolicy::Expanding : QSizePolicy::Maximum); + updateGeometry(); }); } @@ -70,6 +77,7 @@ Section::Section(const QString& title, SectionHeaderMode header_mode, QWidget* p style::metrics::section_body_padding, 0); body_layout_->setSpacing(style::metrics::padding); + body_->setMinimumHeight(0); layout->addWidget(body_); } @@ -98,4 +106,13 @@ void Section::setExpanded(bool expanded) { toggle_button_->setChecked(expanded); } +void Section::setBodyExpanding(bool expanding) { + body_expanding_ = expanding; + body_->setSizePolicy(QSizePolicy::Preferred, expanding ? QSizePolicy::Expanding : QSizePolicy::Preferred); + body_layout_->setStretch(0, expanding ? 1 : 0); + setSizePolicy(QSizePolicy::Preferred, + expanding && isExpanded() ? QSizePolicy::Expanding : QSizePolicy::Maximum); + updateGeometry(); +} + } // namespace ifcinterface::components diff --git a/src/interface/components/Section.h b/src/interface/components/Section.h index bf206b500a..8452694b8a 100644 --- a/src/interface/components/Section.h +++ b/src/interface/components/Section.h @@ -46,12 +46,14 @@ public: void addHeaderWidget(QWidget* widget); bool isExpanded() const; void setExpanded(bool expanded); + void setBodyExpanding(bool expanding); private: QWidget* body_ = nullptr; QVBoxLayout* body_layout_ = nullptr; QHBoxLayout* header_layout_ = nullptr; QToolButton* toggle_button_ = nullptr; + bool body_expanding_ = false; }; } // namespace ifcinterface::components diff --git a/src/interface/components/Style.cpp b/src/interface/components/Style.cpp index 2dd6664f9c..f9ce108713 100644 --- a/src/interface/components/Style.cpp +++ b/src/interface/components/Style.cpp @@ -87,15 +87,13 @@ QString buildAppStyleSheet() { border-right: none; } QLabel#ribbonGroupLabel { - font-size: 9px; + font-size: ${font_small}px; font-weight: 600; - letter-spacing: 0.08em; } QToolButton#ribbonButton { background: transparent; border: none; padding: 6px 4px 4px 4px; - font-size: 11px; color: ${primary_text}; } QToolButton#ribbonButton:hover { @@ -144,9 +142,8 @@ QString buildAppStyleSheet() { } QLabel#panelTitleText { color: ${primary_text}; - font-size: 10px; - font-weight: 700; - letter-spacing: 0.08em; + font-size: ${font_small}px; + font-weight: 600; } QToolButton#panelTitleButton { color: ${panel_title_button}; @@ -168,6 +165,12 @@ QString buildAppStyleSheet() { outline: none; gridline-color: ${border}; } + QTableWidget#modelCoordinatesTable { + font-size: ${font_small}px; + } + QTableWidget#modelCoordinatesTable QHeaderView::section { + font-size: ${font_small}px; + } QTreeWidget::viewport, QListWidget::viewport, QTableWidget::viewport { background: ${panel_background}; } @@ -463,6 +466,7 @@ QString buildAppStyleSheet() { stylesheet.replace("${panel_radius}", QString::number(metrics::panel_radius)); stylesheet.replace("${padding}", QString::number(metrics::padding)); stylesheet.replace("${section_header_padding}", QString::number(metrics::section_header_padding)); + stylesheet.replace("${font_small}", QString::number(typography::small)); stylesheet.replace("${app_background}", palette::app_background); stylesheet.replace("${border}", palette::border); diff --git a/src/interface/components/Style.h b/src/interface/components/Style.h index 84f6426d3e..3dc1dd0b20 100644 --- a/src/interface/components/Style.h +++ b/src/interface/components/Style.h @@ -32,6 +32,12 @@ inline constexpr int panel_radius = 3; } // namespace ifcinterface::components::style::metrics +namespace ifcinterface::components::style::typography { + +inline constexpr int small = 10; + +} // namespace ifcinterface::components::style::typography + namespace ifcinterface::components::style::palette { inline constexpr auto app_background = "#26292f"; diff --git a/src/interface/main.cpp b/src/interface/main.cpp index 353d587f01..91db90afd5 100644 --- a/src/interface/main.cpp +++ b/src/interface/main.cpp @@ -30,14 +30,8 @@ namespace { void installUiFont() { - const QString blender_font = - "/home/dion/drive/blender/blender-5.1.0-linux-x64/5.1/datafiles/fonts/Inter.woff2"; - - int font_id = QFontDatabase::addApplicationFont(blender_font); - if (font_id < 0) { - font_id = QFontDatabase::addApplicationFont( - ":/fonts/DMSans-VariableFont_opsz,wght.ttf"); - } + const int font_id = QFontDatabase::addApplicationFont( + ":/fonts/DMSans-VariableFont_opsz,wght.ttf"); QString family; if (font_id >= 0) { const QStringList families = QFontDatabase::applicationFontFamilies(font_id); diff --git a/src/interface/modules/models/Controller.cpp b/src/interface/modules/models/Controller.cpp index 7a13ba8609..f39c3c8a79 100644 --- a/src/interface/modules/models/Controller.cpp +++ b/src/interface/modules/models/Controller.cpp @@ -216,7 +216,7 @@ void ModelsPanelController::removeLoadedModel(const QString& fed_id) { } void ModelsPanelController::openSettings() { - SettingsDialog dialog(session_state_->federation(), host_); + SettingsDialog dialog(session_state_, host_); dialog.exec(); } diff --git a/src/interface/modules/models/Panel.cpp b/src/interface/modules/models/Panel.cpp index c55a2d2dbf..47b0e50efa 100644 --- a/src/interface/modules/models/Panel.cpp +++ b/src/interface/modules/models/Panel.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include @@ -36,8 +37,9 @@ ModelsPanel::ModelsPanel(QWidget* parent) : components::Panel("Models", nullptr, parent, true) { auto* section = new components::Section("", components::SectionHeaderMode::Hidden, this); - + section->setBodyExpanding(true); tree_ = new QTreeWidget(section); + tree_->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); tree_->setColumnCount(2); tree_->setHeaderLabels({"Model", ""}); tree_->setIconSize(QSize(16, 16)); diff --git a/src/interface/modules/models/SettingsDialog.cpp b/src/interface/modules/models/SettingsDialog.cpp index 5938c752f0..8754c69d5a 100644 --- a/src/interface/modules/models/SettingsDialog.cpp +++ b/src/interface/modules/models/SettingsDialog.cpp @@ -20,24 +20,33 @@ #include "SettingsDialog.h" +#include "../../SessionState.h" #include "../../../ifcviewer/Federation.h" +#include "../../../ifcviewer/SceneLoader.h" #include "../../components/Section.h" #include "../../components/Style.h" #include "../../components/SvgIcon.h" #include "../../components/Tabs.h" -#include #include #include #include +#include #include #include #include #include -#include +#include +#include #include +#include +#include +#include +#include #include +#include + namespace ifcinterface::modules::models { namespace { @@ -59,13 +68,6 @@ const UnitChoice kUnitChoices[] = { {"Miles (mi)", "", "mile"}, }; -QString unitDisplay(const FederationConfig& cfg) { - QString s; - if (!cfg.unit_prefix.empty()) s += QString::fromStdString(cfg.unit_prefix) + " "; - s += QString::fromStdString(cfg.unit_name); - return s; -} - QLineEdit* makeNumericField(QWidget* parent, const QString& placeholder = {}) { auto* field = new QLineEdit(parent); field->setPlaceholderText(placeholder); @@ -73,6 +75,19 @@ QLineEdit* makeNumericField(QWidget* parent, const QString& placeholder = {}) { return field; } +QWidget* makeEqualThirdsRow(QWidget* parent, QLineEdit* a, QLineEdit* b, QLineEdit* c) { + auto* row = new QWidget(parent); + auto* layout = new QHBoxLayout(row); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(components::style::metrics::padding); + for (QLineEdit* field : {a, b, c}) { + field->setMaximumWidth(QWIDGETSIZE_MAX); + field->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + layout->addWidget(field, 1); + } + return row; +} + QString formatNumber(double value) { return QString::number(value, 'f', 6); } @@ -83,57 +98,62 @@ double parseNumber(QLineEdit* field) { return ok ? value : 0.0; } -QWidget* makeVector3Row(QLineEdit* x, - QLineEdit* y, - QLineEdit* z, - QWidget* parent) { - auto* row = new QWidget(parent); - auto* layout = new QHBoxLayout(row); - layout->setContentsMargins(0, 0, 0, 0); - layout->setSpacing(components::style::metrics::padding); - layout->addWidget(x); - layout->addWidget(y); - layout->addWidget(z); - layout->addStretch(1); - return row; +QString formatVector3(const Eigen::Vector3d& value) { + return QString("%1, %2, %3").arg(formatNumber(value.x()), formatNumber(value.y()), formatNumber(value.z())); } -QWidget* makeRotationRow(QLineEdit* rx, - QLineEdit* ry, - QLineEdit* rz, - QWidget* parent) { - auto* row = new QWidget(parent); - auto* layout = new QHBoxLayout(row); - layout->setContentsMargins(0, 0, 0, 0); - layout->setSpacing(components::style::metrics::padding); - layout->addWidget(rx); - layout->addWidget(ry); - layout->addWidget(rz); - layout->addStretch(1); - return row; +Eigen::Vector3d parseVector3(const QString& text) { + const QStringList parts = text.split(QRegularExpression("[,\\s]+"), Qt::SkipEmptyParts); + if (parts.size() != 3) return Eigen::Vector3d::Zero(); + + bool ok_x = false; + bool ok_y = false; + bool ok_z = false; + const double x = parts[0].toDouble(&ok_x); + const double y = parts[1].toDouble(&ok_y); + const double z = parts[2].toDouble(&ok_z); + if (!ok_x || !ok_y || !ok_z) return Eigen::Vector3d::Zero(); + return Eigen::Vector3d(x, y, z); +} + +QString formatAngleDms(double degrees) { + const double absolute = std::fabs(degrees); + const int d = static_cast(absolute); + const double minutes_total = (absolute - static_cast(d)) * 60.0; + const int m = static_cast(minutes_total); + const double s = (minutes_total - static_cast(m)) * 60.0; + const QString sign = degrees < 0.0 ? "-" : ""; + return QString("%1%2° %3' %4\"").arg(sign).arg(d).arg(m, 2, 10, QChar('0')).arg(formatNumber(s)); +} + +QLabel* makeReadOnlyValue(QWidget* parent) { + auto* label = new QLabel(parent); + label->setTextInteractionFlags(Qt::TextSelectableByMouse); + label->setWordWrap(true); + return label; } } // namespace -SettingsDialog::SettingsDialog(Federation* federation, QWidget* parent) - : components::Dialog(parent) - , federation_(federation) +SettingsDialog::SettingsDialog(ifcinterface::SessionState* session_state, QWidget* parent) + : components::Dialog(parent, true) + , session_state_(session_state) + , federation_(session_state ? session_state->federation() : nullptr) + , loader_(session_state ? session_state->loader() : nullptr) { setObjectName("appDialog"); setWindowTitle("Model Settings"); setModal(true); - resize(560, 560); + resize(980, 560); setStyleSheet(components::style::buildAppStyleSheet()); setupUi(); } void SettingsDialog::showEvent(QShowEvent* event) { + federation_ = session_state_ ? session_state_->federation() : federation_; + loader_ = session_state_ ? session_state_->loader() : loader_; syncFromFederation(); - populateModelCombo(); - refreshUnitLabels(); - if (model_combo_->count() > 0) { - syncFromModel(model_combo_->currentData().toString()); - } + populateModelTable(); QDialog::showEvent(event); } @@ -144,8 +164,15 @@ void SettingsDialog::setupUi() { auto* federation_layout = new QVBoxLayout(federation_tab); federation_layout->setContentsMargins(0, 0, 0, 0); federation_layout->setSpacing(components::style::metrics::padding); + federation_layout->setAlignment(Qt::AlignTop); - auto* federation_unit_section = new components::Section("Federation Unit", components::SectionHeaderMode::Visible, federation_tab); + auto* federation_unit_section = + new components::Section("Federation Unit", components::SectionHeaderMode::Visible, federation_tab); + auto* unit_hint = new QLabel( + "All measurements, federated false origins, and destination model transforms will be interpreted in this unit.", + federation_unit_section); + unit_hint->setProperty("textRole", "secondary"); + unit_hint->setWordWrap(true); auto* federation_unit_body = new QWidget(federation_unit_section); auto* federation_unit_form = new QFormLayout(federation_unit_body); federation_unit_form->setContentsMargins(0, 0, 0, 0); @@ -158,15 +185,16 @@ void SettingsDialog::setupUi() { unit_combo_->addItem(uc.label, data); } federation_unit_form->addRow("Unit", unit_combo_); - auto* unit_hint = new QLabel( - "All federated false origin and model transformation values are interpreted in this unit.", - federation_unit_body); - unit_hint->setProperty("textRole", "secondary"); - unit_hint->setWordWrap(true); - federation_unit_form->addRow(QString(), unit_hint); + federation_unit_section->addBodyWidget(unit_hint); federation_unit_section->addBodyWidget(federation_unit_body); - auto* origin_section = new components::Section("Federated False Origin", components::SectionHeaderMode::Visible, federation_tab); + auto* origin_section = + new components::Section("Federated False Origin", components::SectionHeaderMode::Visible, federation_tab); + auto* origin_hint = new QLabel( + "Nominate a false origin and project north to use when viewing the federation of models.", + origin_section); + origin_hint->setProperty("textRole", "secondary"); + origin_hint->setWordWrap(true); auto* origin_body = new QWidget(origin_section); auto* origin_form = new QFormLayout(origin_body); origin_form->setContentsMargins(0, 0, 0, 0); @@ -176,14 +204,9 @@ void SettingsDialog::setupUi() { xyz_y_ = makeNumericField(origin_body, "Y"); xyz_z_ = makeNumericField(origin_body, "Z"); rz_deg_ = makeNumericField(origin_body, "deg"); - origin_form->addRow("XYZ", makeVector3Row(xyz_x_, xyz_y_, xyz_z_, origin_body)); + origin_form->addRow("XYZ", makeEqualThirdsRow(origin_body, xyz_x_, xyz_y_, xyz_z_)); origin_form->addRow("Z rotation (°)", rz_deg_); - auto* origin_hint = new QLabel( - "Nominate a federation point that becomes the new origin, with optional north rotation.", - origin_body); - origin_hint->setProperty("textRole", "secondary"); - origin_hint->setWordWrap(true); - origin_form->addRow(QString(), origin_hint); + origin_section->addBodyWidget(origin_hint); origin_section->addBodyWidget(origin_body); federation_layout->addWidget(federation_unit_section); @@ -194,105 +217,120 @@ void SettingsDialog::setupUi() { auto* model_layout = new QVBoxLayout(model_tab); model_layout->setContentsMargins(0, 0, 0, 0); model_layout->setSpacing(components::style::metrics::padding); + model_layout->setAlignment(Qt::AlignTop); - auto* picker_section = new components::Section("", components::SectionHeaderMode::Hidden, model_tab); - auto* picker_body = new QWidget(picker_section); - auto* picker_form = new QFormLayout(picker_body); - picker_form->setContentsMargins(0, 0, 0, 0); - picker_form->setHorizontalSpacing(16); - picker_form->setVerticalSpacing(10); - model_combo_ = new QComboBox(picker_body); - picker_form->addRow("Current Model", model_combo_); - picker_section->addBodyWidget(picker_body); + auto* georef_section = + new components::Section("Selected Model Georeferencing", components::SectionHeaderMode::Visible, model_tab); + auto* georef_body = new QWidget(georef_section); + auto* georef_layout = new QHBoxLayout(georef_body); + georef_layout->setContentsMargins(0, 0, 0, 0); + georef_layout->setSpacing(16); + georef_layout->setAlignment(Qt::AlignTop); + georef_present_value_ = makeReadOnlyValue(georef_body); + georef_type_value_ = makeReadOnlyValue(georef_body); + georef_easting_value_ = makeReadOnlyValue(georef_body); + georef_northing_value_ = makeReadOnlyValue(georef_body); + georef_height_value_ = makeReadOnlyValue(georef_body); + georef_x_axis_abscissa_value_ = makeReadOnlyValue(georef_body); + georef_x_axis_ordinate_value_ = makeReadOnlyValue(georef_body); + georef_rotation_dd_value_ = makeReadOnlyValue(georef_body); + georef_rotation_dms_value_ = makeReadOnlyValue(georef_body); + georef_scale_value_ = makeReadOnlyValue(georef_body); + georef_factor_x_value_ = makeReadOnlyValue(georef_body); + georef_factor_y_value_ = makeReadOnlyValue(georef_body); + georef_factor_z_value_ = makeReadOnlyValue(georef_body); - auto* a_section = new components::Section("Point A", components::SectionHeaderMode::Visible, model_tab); - auto* a_body = new QWidget(a_section); - auto* a_form = new QFormLayout(a_body); - a_form->setContentsMargins(0, 0, 0, 0); - a_form->setHorizontalSpacing(16); - a_form->setVerticalSpacing(10); - radio_local_ = new QRadioButton("ModelLocal", a_body); - radio_global_ = new QRadioButton("ModelGlobal", a_body); - radio_global_->setChecked(true); - auto* a_frame_group = new QButtonGroup(this); - a_frame_group->addButton(radio_local_); - a_frame_group->addButton(radio_global_); - auto* a_frame_row = new QHBoxLayout(); - a_frame_row->setContentsMargins(0, 0, 0, 0); - a_frame_row->setSpacing(components::style::metrics::padding); - a_frame_row->addWidget(radio_local_); - a_frame_row->addWidget(radio_global_); - a_frame_row->addStretch(1); - a_form->addRow("Frame", a_frame_row); - a_x_ = makeNumericField(a_body, "X"); - a_y_ = makeNumericField(a_body, "Y"); - a_z_ = makeNumericField(a_body, "Z"); - a_form->addRow("XYZ", makeVector3Row(a_x_, a_y_, a_z_, a_body)); - a_unit_label_ = new QLabel(a_body); - a_unit_label_->setProperty("textRole", "secondary"); - a_form->addRow("Unit", a_unit_label_); - a_section->addBodyWidget(a_body); + auto* georef_col_1 = new QFormLayout(); + georef_col_1->setContentsMargins(0, 0, 0, 0); + georef_col_1->setHorizontalSpacing(12); + georef_col_1->setVerticalSpacing(8); + georef_col_1->addRow("Georeferenced", georef_present_value_); + georef_col_1->addRow("Coordinate Operation", georef_type_value_); + georef_col_1->addRow("Easting", georef_easting_value_); + georef_col_1->addRow("Northing", georef_northing_value_); + georef_col_1->addRow("OrthogonalHeight", georef_height_value_); - auto* b_section = new components::Section("Point B", components::SectionHeaderMode::Visible, model_tab); - auto* b_body = new QWidget(b_section); - auto* b_form = new QFormLayout(b_body); - b_form->setContentsMargins(0, 0, 0, 0); - b_form->setHorizontalSpacing(16); - b_form->setVerticalSpacing(10); - b_x_ = makeNumericField(b_body, "X"); - b_y_ = makeNumericField(b_body, "Y"); - b_z_ = makeNumericField(b_body, "Z"); - b_form->addRow("XYZ", makeVector3Row(b_x_, b_y_, b_z_, b_body)); - b_unit_label_ = new QLabel(b_body); - b_unit_label_->setProperty("textRole", "secondary"); - b_form->addRow("Unit", b_unit_label_); - b_section->addBodyWidget(b_body); + auto* georef_col_2 = new QFormLayout(); + georef_col_2->setContentsMargins(0, 0, 0, 0); + georef_col_2->setHorizontalSpacing(12); + georef_col_2->setVerticalSpacing(8); + georef_col_2->addRow("XAxisAbscissa", georef_x_axis_abscissa_value_); + georef_col_2->addRow("XAxisOrdinate", georef_x_axis_ordinate_value_); + georef_col_2->addRow("Rotation (DD)", georef_rotation_dd_value_); + georef_col_2->addRow("Rotation (DMS)", georef_rotation_dms_value_); - auto* rotation_section = new components::Section("Rotation", components::SectionHeaderMode::Visible, model_tab); - auto* rotation_body = new QWidget(rotation_section); - auto* rotation_form = new QFormLayout(rotation_body); - rotation_form->setContentsMargins(0, 0, 0, 0); - rotation_form->setHorizontalSpacing(16); - rotation_form->setVerticalSpacing(10); - rx_ = makeNumericField(rotation_body, "rx"); - ry_ = makeNumericField(rotation_body, "ry"); - rz_ = makeNumericField(rotation_body, "rz"); - rotation_form->addRow("RXYZ", makeRotationRow(rx_, ry_, rz_, rotation_body)); - rotation_section->addBodyWidget(rotation_body); + auto* georef_col_3 = new QFormLayout(); + georef_col_3->setContentsMargins(0, 0, 0, 0); + georef_col_3->setHorizontalSpacing(12); + georef_col_3->setVerticalSpacing(8); + georef_col_3->addRow("Scale", georef_scale_value_); + georef_col_3->addRow("FactorX", georef_factor_x_value_); + georef_col_3->addRow("FactorY", georef_factor_y_value_); + georef_col_3->addRow("FactorZ", georef_factor_z_value_); - auto* pivot_section = new components::Section("Rotation Pivot", components::SectionHeaderMode::Visible, model_tab); - auto* pivot_body = new QWidget(pivot_section); - auto* pivot_form = new QFormLayout(pivot_body); - pivot_form->setContentsMargins(0, 0, 0, 0); - pivot_form->setHorizontalSpacing(16); - pivot_form->setVerticalSpacing(10); - pivot_x_ = makeNumericField(pivot_body, "X"); - pivot_y_ = makeNumericField(pivot_body, "Y"); - pivot_z_ = makeNumericField(pivot_body, "Z"); - pivot_form->addRow("XYZ", makeVector3Row(pivot_x_, pivot_y_, pivot_z_, pivot_body)); - pivot_unit_label_ = new QLabel(pivot_body); - pivot_unit_label_->setProperty("textRole", "secondary"); - pivot_form->addRow("Unit", pivot_unit_label_); - pivot_section->addBodyWidget(pivot_body); + auto* georef_col_1_widget = new QWidget(georef_body); + georef_col_1_widget->setLayout(georef_col_1); + auto* georef_col_2_widget = new QWidget(georef_body); + georef_col_2_widget->setLayout(georef_col_2); + auto* georef_col_3_widget = new QWidget(georef_body); + georef_col_3_widget->setLayout(georef_col_3); - model_layout->addWidget(picker_section); - model_layout->addWidget(a_section); - model_layout->addWidget(b_section); - model_layout->addWidget(rotation_section); - model_layout->addWidget(pivot_section); - model_layout->addStretch(1); + georef_layout->addWidget(georef_col_1_widget, 1); + georef_layout->addWidget(georef_col_2_widget, 1); + georef_layout->addWidget(georef_col_3_widget, 1); + georef_section->addBodyWidget(georef_body); + + auto* table_section = + new components::Section("Model Transformations", components::SectionHeaderMode::Visible, model_tab); + auto* table_body = new QWidget(table_section); + auto* table_layout = new QVBoxLayout(table_body); + table_layout->setContentsMargins(0, 0, 0, 0); + table_layout->setSpacing(components::style::metrics::padding); + + model_table_ = new QTableWidget(table_body); + model_table_->setObjectName("modelCoordinatesTable"); + model_table_->setColumnCount(6); + model_table_->setHorizontalHeaderLabels( + {"Model", "From", "From Point", "To Point", "Rotate", "Pivot Point"}); + model_table_->verticalHeader()->setVisible(false); + model_table_->horizontalHeader()->setStretchLastSection(false); + model_table_->horizontalHeader()->setSectionsMovable(false); + model_table_->horizontalHeader()->setSectionsClickable(true); + model_table_->horizontalHeader()->setSectionResizeMode(QHeaderView::Interactive); + model_table_->horizontalHeader()->resizeSection(0, 180); + model_table_->horizontalHeader()->resizeSection(1, 150); + model_table_->horizontalHeader()->resizeSection(2, 180); + model_table_->horizontalHeader()->resizeSection(3, 180); + model_table_->horizontalHeader()->resizeSection(4, 180); + model_table_->horizontalHeader()->resizeSection(5, 180); + model_table_->setSelectionBehavior(QAbstractItemView::SelectRows); + model_table_->setSelectionMode(QAbstractItemView::SingleSelection); + model_table_->setShowGrid(true); + model_table_->setWordWrap(false); + model_table_->setEditTriggers(QAbstractItemView::DoubleClicked | QAbstractItemView::EditKeyPressed); + model_table_->setAlternatingRowColors(false); + model_table_->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + + auto* table_hint = new QLabel( + "This section lets you override model coordinates by specifying an optional translation from a point to " + "another desired point, and an optional rotation.", + table_section); + table_hint->setProperty("textRole", "secondary"); + table_hint->setWordWrap(true); + + table_layout->addWidget(model_table_, 1); + table_section->addBodyWidget(table_hint); + table_section->addBodyWidget(table_body); + model_layout->addWidget(georef_section); + model_layout->addWidget(table_section); tabs->addTab(federation_tab, "Federation"); tabs->addTab(model_tab, "Model"); - connect(model_combo_, QOverload::of(&QComboBox::currentIndexChanged), this, - [this](int) { - if (model_combo_->count() > 0) { - syncFromModel(model_combo_->currentData().toString()); - } + connect(model_table_, &QTableWidget::currentCellChanged, this, + [this](int /*current_row*/, int /*current_column*/, int /*previous_row*/, int /*previous_column*/) { + updateSelectedModelGeoref(); }); - connect(radio_local_, &QRadioButton::toggled, this, [this]() { onAFrameToggled(); }); - connect(radio_global_, &QRadioButton::toggled, this, [this]() { onAFrameToggled(); }); auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); if (auto* ok = buttons->button(QDialogButtonBox::Ok)) { @@ -306,11 +344,8 @@ void SettingsDialog::setupUi() { connect(buttons, &QDialogButtonBox::accepted, this, [this]() { onAccepted(); }); connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); - auto* actions_section = new components::Section("", components::SectionHeaderMode::Hidden, this); - actions_section->addBodyWidget(buttons); - addBodyWidget(tabs); - addBodyWidget(actions_section); + addFooterWidget(buttons); } void SettingsDialog::syncFromFederation() { @@ -336,63 +371,133 @@ void SettingsDialog::syncFromFederation() { rz_deg_->setText(formatNumber(origin.rz_deg)); } -void SettingsDialog::populateModelCombo() { - model_combo_->blockSignals(true); - QString previous = model_combo_->currentData().toString(); - model_combo_->clear(); - if (federation_) { - for (const auto& model : federation_->models()) { - model_combo_->addItem(model.display_name.isEmpty() ? model.id : model.display_name, model.id); - } - } - int restore = -1; - for (int i = 0; i < model_combo_->count(); ++i) { - if (model_combo_->itemData(i).toString() == previous) { - restore = i; - break; - } - } - if (restore >= 0) model_combo_->setCurrentIndex(restore); - model_combo_->blockSignals(false); -} - -void SettingsDialog::syncFromModel(const QString& fed_id) { +void SettingsDialog::populateModelTable() { + model_rows_.clear(); + const QSignalBlocker blocker(model_table_); + model_table_->clearContents(); + model_table_->setRowCount(0); if (!federation_) return; - const Federation::Model* model = federation_->findById(fed_id); - if (!model) return; - const auto& xf = model->model_transformation; - radio_local_->setChecked(xf.a_frame == AFrame::ModelLocal); - radio_global_->setChecked(xf.a_frame == AFrame::ModelGlobal); - a_x_->setText(formatNumber(xf.a.x())); - a_y_->setText(formatNumber(xf.a.y())); - a_z_->setText(formatNumber(xf.a.z())); - b_x_->setText(formatNumber(xf.b.x())); - b_y_->setText(formatNumber(xf.b.y())); - b_z_->setText(formatNumber(xf.b.z())); - rx_->setText(formatNumber(xf.rxyz_deg.x())); - ry_->setText(formatNumber(xf.rxyz_deg.y())); - rz_->setText(formatNumber(xf.rxyz_deg.z())); - pivot_x_->setText(formatNumber(xf.pivot.x())); - pivot_y_->setText(formatNumber(xf.pivot.y())); - pivot_z_->setText(formatNumber(xf.pivot.z())); -} + int row = 0; + for (const auto& model : federation_->models()) { + const auto& xf = model.model_transformation; + model_table_->insertRow(row); -void SettingsDialog::refreshUnitLabels() { - if (!federation_) return; - const QString fed_unit = unitDisplay(federation_->config()); - b_unit_label_->setText("(federation: " + fed_unit + ")"); - pivot_unit_label_->setText("(federation: " + fed_unit + ")"); - onAFrameToggled(); -} + auto* model_item = new QTableWidgetItem(model.display_name.isEmpty() ? model.id : model.display_name); + model_item->setData(Qt::UserRole, model.id); + model_table_->setItem(row, 0, model_item); -void SettingsDialog::onAFrameToggled() { - if (!a_unit_label_) return; - if (radio_local_->isChecked()) { - a_unit_label_->setText("(model project length unit)"); - } else { - a_unit_label_->setText("(model map unit)"); + ModelRowWidgets widgets; + widgets.fed_id = model.id; + + widgets.frame = new QComboBox(model_table_); + widgets.frame->addItem("Local", static_cast(AFrame::ModelLocal)); + widgets.frame->addItem("Global", static_cast(AFrame::ModelGlobal)); + widgets.frame->setCurrentIndex(xf.a_frame == AFrame::ModelGlobal ? 1 : 0); + model_table_->setCellWidget(row, 1, widgets.frame); + + widgets.from_point = new QTableWidgetItem(formatVector3(xf.a)); + widgets.to_point = new QTableWidgetItem(formatVector3(xf.b)); + widgets.rotate = new QTableWidgetItem(formatVector3(xf.rxyz_deg)); + widgets.pivot = new QTableWidgetItem(formatVector3(xf.pivot)); + model_table_->setItem(row, 2, widgets.from_point); + model_table_->setItem(row, 3, widgets.to_point); + model_table_->setItem(row, 4, widgets.rotate); + model_table_->setItem(row, 5, widgets.pivot); + + model_rows_.push_back(widgets); + model_table_->setRowHeight(row, 42); + ++row; } + + int table_height = model_table_->frameWidth() * 2 + model_table_->horizontalHeader()->height(); + for (int i = 0; i < model_table_->rowCount(); ++i) { + table_height += model_table_->rowHeight(i); + } + if (model_table_->horizontalScrollBar()->isVisible()) { + table_height += model_table_->horizontalScrollBar()->sizeHint().height(); + } + model_table_->setMinimumHeight(table_height); + model_table_->setMaximumHeight(table_height); + + if (model_table_->rowCount() > 0) { + model_table_->setCurrentCell(0, 0); + } + updateSelectedModelGeoref(); +} + +void SettingsDialog::updateSelectedModelGeoref() { + auto set_unknown = [this](const QString& georef, const QString& type) { + georef_present_value_->setText(georef); + georef_type_value_->setText(type); + georef_easting_value_->setText("—"); + georef_northing_value_->setText("—"); + georef_height_value_->setText("—"); + georef_x_axis_abscissa_value_->setText("—"); + georef_x_axis_ordinate_value_->setText("—"); + georef_rotation_dd_value_->setText("—"); + georef_rotation_dms_value_->setText("—"); + georef_scale_value_->setText("—"); + georef_factor_x_value_->setText("—"); + georef_factor_y_value_->setText("—"); + georef_factor_z_value_->setText("—"); + }; + + const int row = model_table_->currentRow(); + if (row < 0 || row >= static_cast(model_rows_.size())) { + set_unknown("No model selected", "—"); + return; + } + + const QString& fed_id = model_rows_[row].fed_id; + if (!session_state_) { + set_unknown("Unavailable", "No session state"); + return; + } + + const uint32_t mid = session_state_->modelIdForFedId(fed_id); + if (mid == 0 || !loader_) { + set_unknown("Not loaded", "No live model"); + return; + } + + const ModelGeoref* georef = loader_->modelGeoref(mid); + if (!georef) { + set_unknown("Not available yet", "No data source"); + return; + } + + if (!georef->has_coordinate_operation) { + set_unknown("No", "None"); + return; + } + + const Eigen::Matrix4d& m = georef->coordinate_operation_meters; + const Eigen::Vector3d translation = m.block<3, 1>(0, 3); + const Eigen::Vector3d x_axis = m.block<3, 1>(0, 0); + const Eigen::Vector3d y_axis = m.block<3, 1>(0, 1); + const double factor_x = x_axis.norm(); + const double factor_y = y_axis.norm(); + const double factor_z = m.block<3, 1>(0, 2).norm(); + const double scale = (factor_x + factor_y) * 0.5; + const double x_axis_abscissa = factor_x > 0.0 ? x_axis.x() / factor_x : 1.0; + const double x_axis_ordinate = factor_x > 0.0 ? x_axis.y() / factor_x : 0.0; + constexpr double kRadiansToDegrees = 57.29577951308232; + const double rotation_dd = std::atan2(x_axis_ordinate, x_axis_abscissa) * kRadiansToDegrees; + + georef_present_value_->setText("Yes"); + georef_type_value_->setText("IfcMapConversion"); + georef_easting_value_->setText(formatNumber(translation.x())); + georef_northing_value_->setText(formatNumber(translation.y())); + georef_height_value_->setText(formatNumber(translation.z())); + georef_x_axis_abscissa_value_->setText(formatNumber(x_axis_abscissa)); + georef_x_axis_ordinate_value_->setText(formatNumber(x_axis_ordinate)); + georef_rotation_dd_value_->setText(formatNumber(rotation_dd)); + georef_rotation_dms_value_->setText(formatAngleDms(rotation_dd)); + georef_scale_value_->setText(formatNumber(scale)); + georef_factor_x_value_->setText(formatNumber(factor_x)); + georef_factor_y_value_->setText(formatNumber(factor_y)); + georef_factor_z_value_->setText(formatNumber(factor_z)); } void SettingsDialog::onAccepted() { @@ -410,14 +515,14 @@ void SettingsDialog::onAccepted() { origin.rz_deg = parseNumber(rz_deg_); federation_->setFederatedFalseOrigin(origin); - if (model_combo_->count() > 0) { + for (const auto& row : model_rows_) { ModelTransformation xf; - xf.a_frame = radio_local_->isChecked() ? AFrame::ModelLocal : AFrame::ModelGlobal; - xf.a = Eigen::Vector3d(parseNumber(a_x_), parseNumber(a_y_), parseNumber(a_z_)); - xf.b = Eigen::Vector3d(parseNumber(b_x_), parseNumber(b_y_), parseNumber(b_z_)); - xf.rxyz_deg = Eigen::Vector3d(parseNumber(rx_), parseNumber(ry_), parseNumber(rz_)); - xf.pivot = Eigen::Vector3d(parseNumber(pivot_x_), parseNumber(pivot_y_), parseNumber(pivot_z_)); - federation_->setModelTransformation(model_combo_->currentData().toString(), xf); + xf.a_frame = static_cast(row.frame->currentData().toInt()); + xf.a = parseVector3(row.from_point->text()); + xf.b = parseVector3(row.to_point->text()); + xf.rxyz_deg = parseVector3(row.rotate->text()); + xf.pivot = parseVector3(row.pivot->text()); + federation_->setModelTransformation(row.fed_id, xf); } } accept(); diff --git a/src/interface/modules/models/SettingsDialog.h b/src/interface/modules/models/SettingsDialog.h index 86477dd5f3..4ad19f320f 100644 --- a/src/interface/modules/models/SettingsDialog.h +++ b/src/interface/modules/models/SettingsDialog.h @@ -24,34 +24,50 @@ #include "../../components/Dialog.h" #include +#include class Federation; +class SceneLoader; class QComboBox; class QLabel; class QLineEdit; -class QRadioButton; class QShowEvent; +class QTableWidget; +class QTableWidgetItem; + +namespace ifcinterface { +class SessionState; +} namespace ifcinterface::modules::models { class SettingsDialog : public components::Dialog { Q_OBJECT public: - explicit SettingsDialog(Federation* federation, QWidget* parent = nullptr); + explicit SettingsDialog(ifcinterface::SessionState* session_state, QWidget* parent = nullptr); protected: void showEvent(QShowEvent* event) override; private: + struct ModelRowWidgets { + QString fed_id; + QComboBox* frame = nullptr; + QTableWidgetItem* from_point = nullptr; + QTableWidgetItem* to_point = nullptr; + QTableWidgetItem* rotate = nullptr; + QTableWidgetItem* pivot = nullptr; + }; + void setupUi(); void syncFromFederation(); - void populateModelCombo(); - void syncFromModel(const QString& fed_id); - void refreshUnitLabels(); - void onAFrameToggled(); + void populateModelTable(); + void updateSelectedModelGeoref(); void onAccepted(); + ifcinterface::SessionState* session_state_ = nullptr; Federation* federation_ = nullptr; + SceneLoader* loader_ = nullptr; QComboBox* unit_combo_ = nullptr; QLineEdit* xyz_x_ = nullptr; @@ -59,24 +75,22 @@ private: QLineEdit* xyz_z_ = nullptr; QLineEdit* rz_deg_ = nullptr; - QComboBox* model_combo_ = nullptr; - QRadioButton* radio_local_ = nullptr; - QRadioButton* radio_global_ = nullptr; - QLineEdit* a_x_ = nullptr; - QLineEdit* a_y_ = nullptr; - QLineEdit* a_z_ = nullptr; - QLabel* a_unit_label_ = nullptr; - QLineEdit* b_x_ = nullptr; - QLineEdit* b_y_ = nullptr; - QLineEdit* b_z_ = nullptr; - QLabel* b_unit_label_ = nullptr; - QLineEdit* rx_ = nullptr; - QLineEdit* ry_ = nullptr; - QLineEdit* rz_ = nullptr; - QLineEdit* pivot_x_ = nullptr; - QLineEdit* pivot_y_ = nullptr; - QLineEdit* pivot_z_ = nullptr; - QLabel* pivot_unit_label_ = nullptr; + QLabel* georef_present_value_ = nullptr; + QLabel* georef_type_value_ = nullptr; + QLabel* georef_easting_value_ = nullptr; + QLabel* georef_northing_value_ = nullptr; + QLabel* georef_height_value_ = nullptr; + QLabel* georef_x_axis_abscissa_value_ = nullptr; + QLabel* georef_x_axis_ordinate_value_ = nullptr; + QLabel* georef_rotation_dd_value_ = nullptr; + QLabel* georef_rotation_dms_value_ = nullptr; + QLabel* georef_scale_value_ = nullptr; + QLabel* georef_factor_x_value_ = nullptr; + QLabel* georef_factor_y_value_ = nullptr; + QLabel* georef_factor_z_value_ = nullptr; + + QTableWidget* model_table_ = nullptr; + std::vector model_rows_; }; } // namespace ifcinterface::modules::models diff --git a/src/interface/modules/settings/Dialog.cpp b/src/interface/modules/settings/Dialog.cpp index 21bea7a649..6da4ac7b6e 100644 --- a/src/interface/modules/settings/Dialog.cpp +++ b/src/interface/modules/settings/Dialog.cpp @@ -28,6 +28,7 @@ #include "../../components/Tabs.h" #include +#include #include #include #include @@ -110,18 +111,85 @@ void SettingsDialog::setupUi() { angular_tolerance_spin_->setSingleStep(0.05); loading_form->addRow("Angular Tolerance", angular_tolerance_spin_); - auto* description = new QLabel( - "These settings are shared with the viewer backend and persist via QSettings.", - loading_body); - description->setProperty("textRole", "secondary"); - description->setWordWrap(true); - loading_form->addRow(QString(), description); + min_pixel_radius_spin_ = new QDoubleSpinBox(loading_body); + min_pixel_radius_spin_->setRange(0.0, 100.0); + min_pixel_radius_spin_->setDecimals(2); + min_pixel_radius_spin_->setSingleStep(0.5); + min_pixel_radius_spin_->setToolTip( + "Minimum projected sphere radius (in pixels) for an instance to " + "be drawn. Bigger = faster but more pop-in on small detail."); + loading_form->addRow("Min Pixel Radius", min_pixel_radius_spin_); + + motion_min_pixel_radius_spin_ = new QDoubleSpinBox(loading_body); + motion_min_pixel_radius_spin_->setRange(0.0, 100.0); + motion_min_pixel_radius_spin_->setDecimals(2); + motion_min_pixel_radius_spin_->setSingleStep(1.0); + motion_min_pixel_radius_spin_->setToolTip( + "Aggressive cull threshold while the camera is moving. 0 = no " + "motion boost (motion uses the same threshold as still frames)."); + loading_form->addRow("Motion Min Pixel Radius", motion_min_pixel_radius_spin_); + + lod1_pixel_threshold_spin_ = new QDoubleSpinBox(loading_body); + lod1_pixel_threshold_spin_->setRange(0.0, 1000.0); + lod1_pixel_threshold_spin_->setDecimals(1); + lod1_pixel_threshold_spin_->setSingleStep(1.0); + lod1_pixel_threshold_spin_->setToolTip( + "Pixel radius below which an instance switches to its LOD1 " + "representation. 0 disables LOD1 entirely."); + loading_form->addRow("LOD1 Pixel Threshold", lod1_pixel_threshold_spin_); + + hiz_enabled_check_ = new QCheckBox(loading_body); + hiz_enabled_check_->setToolTip( + "Enable HiZ (hierarchical Z) occlusion culling. Hides geometry " + "behind opaque blockers based on a downsampled depth pyramid."); + loading_form->addRow("HiZ Occlusion", hiz_enabled_check_); + + hiz_resolution_spin_ = new QSpinBox(loading_body); + hiz_resolution_spin_->setRange(64, 4096); + hiz_resolution_spin_->setSingleStep(64); + hiz_resolution_spin_->setToolTip( + "Base HiZ pyramid width in texels (height tracks aspect). " + "Changes take effect on next viewport reinitialization."); + loading_form->addRow("HiZ Resolution", hiz_resolution_spin_); loading_section->addBodyWidget(loading_body); graphics_layout->addWidget(general_section); graphics_layout->addWidget(loading_section); graphics_layout->addStretch(1); + // Navigation tab: orbit / pan presets. Selection always stays on + // LMB so click + box-select keep working regardless of preset. + auto* navigation_tab = new QWidget(tabs); + { + auto* layout = new QVBoxLayout(navigation_tab); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(components::style::metrics::padding); + + auto* section = new components::Section( + "Navigation", components::SectionHeaderMode::Visible, navigation_tab); + auto* body = new QWidget(section); + auto* form = new QFormLayout(body); + form->setContentsMargins(0, 0, 0, 0); + form->setHorizontalSpacing(16); + form->setVerticalSpacing(10); + + nav_preset_combo_ = new QComboBox(body); + // Order must match AppSettings::NavPreset enum ordering — index + // is what we read back via currentIndex / setCurrentIndex. + nav_preset_combo_->addItem("Blender (Orbit MMB, Pan Shift+MMB)"); + nav_preset_combo_->addItem("Rhino (Orbit RMB, Pan Shift+RMB)"); + nav_preset_combo_->addItem("Revit (Orbit Shift+MMB, Pan MMB)"); + nav_preset_combo_->setToolTip( + "Mouse-button mapping for orbit and pan. Selection stays on " + "left mouse button for every preset, so click + box-select " + "always work."); + form->addRow("Preset", nav_preset_combo_); + + section->addBodyWidget(body); + layout->addWidget(section); + layout->addStretch(1); + } + auto make_placeholder_tab = [tabs](const QString& title, const QString& detail) { auto* tab = new QWidget(tabs); auto* layout = new QVBoxLayout(tab); @@ -147,8 +215,7 @@ void SettingsDialog::setupUi() { return tab; }; - tabs->addTab(make_placeholder_tab("Navigation", "Navigation preferences and interaction modes will live here."), - "Navigation"); + tabs->addTab(navigation_tab, "Navigation"); tabs->addTab(make_placeholder_tab("Keybindings", "Shortcut presets and command bindings will live here."), "Keybindings"); tabs->addTab(graphics_tab, "Graphics"); @@ -169,11 +236,8 @@ void SettingsDialog::setupUi() { connect(buttons, &QDialogButtonBox::accepted, this, &SettingsDialog::onAccepted); connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); - auto* actions_section = new components::Section("", components::SectionHeaderMode::Hidden, this); - actions_section->addBodyWidget(buttons); - addBodyWidget(tabs); - addBodyWidget(actions_section); + addFooterWidget(buttons); } void SettingsDialog::syncFromSettings() { @@ -183,6 +247,12 @@ void SettingsDialog::syncFromSettings() { void_limit_spin_->setValue(AppSettings::instance().voidLimit()); deflection_tolerance_spin_->setValue(AppSettings::instance().deflectionTolerance()); angular_tolerance_spin_->setValue(AppSettings::instance().angularTolerance()); + min_pixel_radius_spin_->setValue(AppSettings::instance().minPixelRadius()); + motion_min_pixel_radius_spin_->setValue(AppSettings::instance().motionMinPixelRadius()); + lod1_pixel_threshold_spin_->setValue(AppSettings::instance().lod1PixelThreshold()); + hiz_enabled_check_->setChecked(AppSettings::instance().hizEnabled()); + hiz_resolution_spin_->setValue(AppSettings::instance().hizResolution()); + nav_preset_combo_->setCurrentIndex(static_cast(AppSettings::instance().navPreset())); } void SettingsDialog::onAccepted() { @@ -192,6 +262,13 @@ void SettingsDialog::onAccepted() { AppSettings::instance().setVoidLimit(void_limit_spin_->value()); AppSettings::instance().setDeflectionTolerance(deflection_tolerance_spin_->value()); AppSettings::instance().setAngularTolerance(angular_tolerance_spin_->value()); + AppSettings::instance().setMinPixelRadius(min_pixel_radius_spin_->value()); + AppSettings::instance().setMotionMinPixelRadius(motion_min_pixel_radius_spin_->value()); + AppSettings::instance().setLod1PixelThreshold(lod1_pixel_threshold_spin_->value()); + AppSettings::instance().setHizEnabled(hiz_enabled_check_->isChecked()); + AppSettings::instance().setHizResolution(hiz_resolution_spin_->value()); + AppSettings::instance().setNavPreset( + static_cast(nav_preset_combo_->currentIndex())); accept(); } diff --git a/src/interface/modules/settings/Dialog.h b/src/interface/modules/settings/Dialog.h index d13f8d972c..5aed5efe43 100644 --- a/src/interface/modules/settings/Dialog.h +++ b/src/interface/modules/settings/Dialog.h @@ -24,6 +24,7 @@ #include "../../components/Dialog.h" class QCheckBox; +class QComboBox; class QDoubleSpinBox; class QLineEdit; class QShowEvent; @@ -50,6 +51,12 @@ private: QSpinBox* void_limit_spin_ = nullptr; QDoubleSpinBox* deflection_tolerance_spin_ = nullptr; QDoubleSpinBox* angular_tolerance_spin_ = nullptr; + QDoubleSpinBox* min_pixel_radius_spin_ = nullptr; + QDoubleSpinBox* motion_min_pixel_radius_spin_ = nullptr; + QDoubleSpinBox* lod1_pixel_threshold_spin_ = nullptr; + QCheckBox* hiz_enabled_check_ = nullptr; + QSpinBox* hiz_resolution_spin_ = nullptr; + QComboBox* nav_preset_combo_ = nullptr; }; } // namespace ifcinterface::modules::settings