From cb0a70ac7e42ba6534d321cb0481f4dbc69c1bd6 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 10 May 2026 21:55:06 +1000 Subject: [PATCH] Configurable navigation bindings --- src/ifcviewer/AppSettings.cpp | 24 +++++++++ src/ifcviewer/AppSettings.h | 21 ++++++++ src/ifcviewer/ViewportWindow.cpp | 91 +++++++++++++++++++++++--------- src/ifcviewer/ViewportWindow.h | 11 ++++ 4 files changed, 123 insertions(+), 24 deletions(-) diff --git a/src/ifcviewer/AppSettings.cpp b/src/ifcviewer/AppSettings.cpp index 75675074d0..8b8212cb4a 100644 --- a/src/ifcviewer/AppSettings.cpp +++ b/src/ifcviewer/AppSettings.cpp @@ -42,6 +42,7 @@ constexpr const char* kHizResolutionKey = "viewport/hiz_resolution"; constexpr int kHizResolutionDefault = 256; constexpr int kHizResolutionFloor = 64; constexpr const char* kHizEnabledKey = "viewport/hiz_enabled"; +constexpr const char* kNavPresetKey = "viewport/nav_preset"; } AppSettings& AppSettings::instance() { @@ -181,6 +182,17 @@ void AppSettings::setHizEnabled(bool value) { emit hizEnabledChanged(value); } +AppSettings::NavPreset AppSettings::navPreset() const { + return nav_preset_; +} + +void AppSettings::setNavPreset(NavPreset value) { + if (nav_preset_ == value) return; + nav_preset_ = value; + persist(); + emit navPresetChanged(value); +} + void AppSettings::load() { QSettings settings; geometry_library_ = settings.value(kGeometryLibraryKey, kGeometryLibraryDefault).toString(); @@ -203,6 +215,17 @@ void AppSettings::load() { hiz_resolution_ = settings.value(kHizResolutionKey, kHizResolutionDefault).toInt(); if (hiz_resolution_ < kHizResolutionFloor) hiz_resolution_ = kHizResolutionFloor; hiz_enabled_ = settings.value(kHizEnabledKey, true).toBool(); + { + const int raw = settings.value(kNavPresetKey, + static_cast(NavPreset::Blender)).toInt(); + // Clamp to known values so a stale config doesn't drop us into + // an undefined preset slot. + if (raw < 0 || raw > static_cast(NavPreset::Revit)) { + nav_preset_ = NavPreset::Blender; + } else { + nav_preset_ = static_cast(raw); + } + } } void AppSettings::persist() { @@ -218,4 +241,5 @@ void AppSettings::persist() { settings.setValue(kLod1PixelThresholdKey, lod1_pixel_threshold_); settings.setValue(kHizResolutionKey, hiz_resolution_); settings.setValue(kHizEnabledKey, hiz_enabled_); + settings.setValue(kNavPresetKey, static_cast(nav_preset_)); } diff --git a/src/ifcviewer/AppSettings.h b/src/ifcviewer/AppSettings.h index feb94b0f44..9c38819a6a 100644 --- a/src/ifcviewer/AppSettings.h +++ b/src/ifcviewer/AppSettings.h @@ -29,6 +29,21 @@ class AppSettings : public QObject { Q_OBJECT public: + // Navigation preset. Selects which mouse button (+ optional Shift) + // drives orbit and pan. Selection is unaffected and stays on LMB + // for every preset — these three intentionally don't take LMB so + // click + box-select remain available without modifier gymnastics. + // + // Blender — Orbit MMB, Pan Shift+MMB (current default) + // Rhino — Orbit RMB, Pan Shift+RMB + // Revit — Orbit Shift+MMB, Pan MMB + enum class NavPreset { + Blender = 0, + Rhino = 1, + Revit = 2, + }; + Q_ENUM(NavPreset) + static AppSettings& instance(); QString geometryLibrary() const; @@ -89,6 +104,10 @@ public: bool hizEnabled() const; void setHizEnabled(bool value); + // Navigation preset (see NavPreset enum above). + NavPreset navPreset() const; + void setNavPreset(NavPreset value); + signals: void geometryLibraryChanged(const QString& value); void showStatsChanged(bool value); @@ -101,6 +120,7 @@ signals: void lod1PixelThresholdChanged(double value); void hizResolutionChanged(int value); void hizEnabledChanged(bool value); + void navPresetChanged(NavPreset value); private: AppSettings(); @@ -118,6 +138,7 @@ private: double lod1_pixel_threshold_ = 30.0; int hiz_resolution_ = 256; bool hiz_enabled_ = true; + NavPreset nav_preset_ = NavPreset::Blender; }; #endif // APPSETTINGS_H diff --git a/src/ifcviewer/ViewportWindow.cpp b/src/ifcviewer/ViewportWindow.cpp index 3efd8fe904..eef4db6938 100644 --- a/src/ifcviewer/ViewportWindow.cpp +++ b/src/ifcviewer/ViewportWindow.cpp @@ -3572,9 +3572,17 @@ void ViewportWindow::handleMousePress(QMouseEvent* e) { box_select_armed_ = false; box_select_active_ = false; press_pick_id_ = 0; - if (e->button() == Qt::MiddleButton) { - setPivotIndicatorVisible(true); - requestUpdate(); + // Show the pivot indicator while the user holds the navigation + // button(s) for the active preset — Blender uses MMB for both + // orbit + pan, Revit too (with Shift swapping the role), Rhino + // uses RMB. Either way, the press of that button is what the + // user means as "I'm about to navigate". + { + const NavBindings nb = currentNavBindings(); + if (e->button() == nb.orbit_button || e->button() == nb.pan_button) { + setPivotIndicatorVisible(true); + requestUpdate(); + } } if (section_tool_active_ && e->button() == Qt::LeftButton) { // First try to grab an existing plane's arrow gizmo. @@ -3684,7 +3692,9 @@ void ViewportWindow::handleMouseRelease(QMouseEvent* e) { press_pick_id_ = 0; } - const bool was_navigating = (active_button_ == Qt::MiddleButton); + const NavBindings nb = currentNavBindings(); + const bool was_navigating = + (active_button_ == nb.orbit_button || active_button_ == nb.pan_button); active_button_ = Qt::NoButton; if (was_navigating && pivot_indicator_visible_) { setPivotIndicatorVisible(false); @@ -3749,26 +3759,40 @@ void ViewportWindow::handleMouseMove(QMouseEvent* e) { QPoint delta = e->pos() - last_mouse_pos_; last_mouse_pos_ = e->pos(); - if (active_button_ == Qt::MiddleButton) { - if (e->modifiers() & Qt::ShiftModifier) { - const float pan_speed = camera_distance_ * 0.002f; - // Derive screen-right and screen-up from the actual camera basis - // rather than yaw/pitch alone — the latter assumed up = world +Z, - // which breaks at top/bottom views where updateCamera switches - // the lookAt up vector to world +Y. - const QVector3D forward = (camera_target_ - camera_eye_).normalized(); - const QVector3D up_ref = (std::abs(camera_pitch_) >= 89.0f) - ? QVector3D(0, 1, 0) - : QVector3D(0, 0, 1); - const QVector3D right = QVector3D::crossProduct(forward, up_ref).normalized(); - const QVector3D up = QVector3D::crossProduct(right, forward).normalized(); - camera_target_ -= right * delta.x() * pan_speed; - camera_target_ += up * delta.y() * pan_speed; - } else { - camera_yaw_ -= delta.x() * 0.3f; - camera_pitch_ += delta.y() * 0.3f; - camera_pitch_ = qBound(-89.0f, camera_pitch_, 89.0f); - } + + // Dispatch nav drag against the active preset. Modifier matching + // is exact on the Shift bit only — the only modifier any of our + // presets uses — so toggling Shift mid-drag flips between orbit + // and pan when both share a button (Blender / Revit). + const NavBindings nb = currentNavBindings(); + const bool shift_now = (e->modifiers() & Qt::ShiftModifier) != 0; + const bool shift_for_pan = (nb.pan_mods & Qt::ShiftModifier) != 0; + const bool shift_for_orb = (nb.orbit_mods & Qt::ShiftModifier) != 0; + const bool is_pan = (active_button_ == nb.pan_button) + && (shift_now == shift_for_pan); + const bool is_orbit = !is_pan + && (active_button_ == nb.orbit_button) + && (shift_now == shift_for_orb); + + if (is_pan) { + const float pan_speed = camera_distance_ * 0.002f; + // Derive screen-right and screen-up from the actual camera basis + // rather than yaw/pitch alone — the latter assumed up = world +Z, + // which breaks at top/bottom views where updateCamera switches + // the lookAt up vector to world +Y. + const QVector3D forward = (camera_target_ - camera_eye_).normalized(); + const QVector3D up_ref = (std::abs(camera_pitch_) >= 89.0f) + ? QVector3D(0, 1, 0) + : QVector3D(0, 0, 1); + const QVector3D right = QVector3D::crossProduct(forward, up_ref).normalized(); + const QVector3D up = QVector3D::crossProduct(right, forward).normalized(); + camera_target_ -= right * delta.x() * pan_speed; + camera_target_ += up * delta.y() * pan_speed; + requestUpdate(); + } else if (is_orbit) { + camera_yaw_ -= delta.x() * 0.3f; + camera_pitch_ += delta.y() * 0.3f; + camera_pitch_ = qBound(-89.0f, camera_pitch_, 89.0f); requestUpdate(); } } @@ -4249,6 +4273,25 @@ void ViewportWindow::toggleVolumeTool() { setToolMode(tool_mode_ == ToolMode::Volume ? ToolMode::None : ToolMode::Volume); } +ViewportWindow::NavBindings ViewportWindow::currentNavBindings() const { + using NP = AppSettings::NavPreset; + switch (AppSettings::instance().navPreset()) { + case NP::Rhino: + // Orbit RMB, Pan Shift+RMB. + return { Qt::RightButton, Qt::NoModifier, + Qt::RightButton, Qt::ShiftModifier }; + case NP::Revit: + // Orbit Shift+MMB, Pan MMB. + return { Qt::MiddleButton, Qt::ShiftModifier, + Qt::MiddleButton, Qt::NoModifier }; + case NP::Blender: + default: + // Orbit MMB, Pan Shift+MMB. + return { Qt::MiddleButton, Qt::NoModifier, + Qt::MiddleButton, Qt::ShiftModifier }; + } +} + void ViewportWindow::setHighlightTriangles(const std::vector& world_xyz, float r, float g, float b, float a) { if (!gl_initialized_) return; diff --git a/src/ifcviewer/ViewportWindow.h b/src/ifcviewer/ViewportWindow.h index 822d4e55c4..79abb50671 100644 --- a/src/ifcviewer/ViewportWindow.h +++ b/src/ifcviewer/ViewportWindow.h @@ -431,6 +431,17 @@ protected: bool event(QEvent* event) override; private: + // Resolved mouse-binding for the active AppSettings::NavPreset. + // Selection always stays on LMB; only orbit and pan move around + // (presets are picked so neither lands on plain LMB). + struct NavBindings { + Qt::MouseButton orbit_button; + Qt::KeyboardModifiers orbit_mods; + Qt::MouseButton pan_button; + Qt::KeyboardModifiers pan_mods; + }; + NavBindings currentNavBindings() const; + enum class PendingOpType { UploadMeshChunk, UploadInstanceChunk,