From ddef8c65b5b2de916cec3828ae126d10acf16c16 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Wed, 27 May 2026 14:10:51 +1000 Subject: [PATCH] wgpu backend: orbit/pan/zoom mouse navigation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LMB drag → orbit (yaw/pitch, pitch clamped to ±89.9° to avoid gimbal flip at the poles). MMB drag → pan in the camera's screen-space plane, world-units-per-pixel sized against the view frustum at the pivot depth so panning feels constant regardless of zoom. Wheel → zoom (12% per notch, sign matches "wheel up = closer"). LMB is bound to orbit because selection isn't wired yet; will rebind to selection + nav preset once AppSettings ports over. Pure addition to WgpuViewportWindow — overrides four QWindow event handlers, no changes to render or cull paths. Lets you actually fly around a loaded sidecar without a screenshot loop. Co-Authored-By: Claude Opus 4.7 --- src/ifcviewer-wgpu/WgpuViewportWindow.cpp | 74 +++++++++++++++++++++++ src/ifcviewer-wgpu/WgpuViewportWindow.h | 12 ++++ 2 files changed, 86 insertions(+) diff --git a/src/ifcviewer-wgpu/WgpuViewportWindow.cpp b/src/ifcviewer-wgpu/WgpuViewportWindow.cpp index 3bbffc4590..8f003a217a 100644 --- a/src/ifcviewer-wgpu/WgpuViewportWindow.cpp +++ b/src/ifcviewer-wgpu/WgpuViewportWindow.cpp @@ -1331,6 +1331,80 @@ void WgpuViewportWindow::captureNextFrameToPng(const QString& path, bool quit_af if (isExposed()) requestUpdate(); } +// ----------------------------------------------------------------------------- +// Mouse navigation — orbit, pan, zoom +// ----------------------------------------------------------------------------- +// +// LMB drag → orbit (yaw/pitch). MMB drag → pan (target moves in the camera's +// screen-space plane). Wheel → zoom (camera_distance_ multiplies). Pitch is +// clamped just shy of ±90° to avoid the gimbal-flip at the poles. +// +// No nav-preset awareness yet (Blender/Rhino/Revit bindings come later); we +// don't have selection bound, so LMB is free to orbit. + +#include +#include + +void WgpuViewportWindow::mousePressEvent(QMouseEvent* event) { + nav_active_button_ = event->button(); + nav_last_pos_ = event->position().toPoint(); +} + +void WgpuViewportWindow::mouseReleaseEvent(QMouseEvent* event) { + if (event->button() == nav_active_button_) { + nav_active_button_ = Qt::NoButton; + } +} + +void WgpuViewportWindow::mouseMoveEvent(QMouseEvent* event) { + if (nav_active_button_ == Qt::NoButton) return; + + const QPoint pos = event->position().toPoint(); + const int dx = pos.x() - nav_last_pos_.x(); + const int dy = pos.y() - nav_last_pos_.y(); + nav_last_pos_ = pos; + + if (nav_active_button_ == Qt::LeftButton) { + // Orbit. 0.4 deg/px feels right for a 1280-wide window. + camera_yaw_deg_ += float(dx) * -0.4f; + camera_pitch_deg_ += float(dy) * -0.4f; + camera_pitch_deg_ = std::clamp(camera_pitch_deg_, -89.9f, 89.9f); + requestUpdate(); + } else if (nav_active_button_ == Qt::MiddleButton) { + // Pan in the camera's screen-space plane. World units per pixel + // tracks the view-frustum width at the pivot's depth so panning + // feels constant regardless of zoom. + const QVector3D target(camera_target_[0], camera_target_[1], camera_target_[2]); + const QVector3D eye = orbitEye(camera_target_, camera_distance_, + camera_yaw_deg_, camera_pitch_deg_); + const QVector3D fwd = (target - eye).normalized(); + const QVector3D right = QVector3D::crossProduct(fwd, QVector3D(0, 0, 1)).normalized(); + const QVector3D up = QVector3D::crossProduct(right, fwd).normalized(); + + const float half_h_world = camera_distance_ + * std::tan(qDegreesToRadians(camera_fov_y_deg_) * 0.5f); + const float pan_per_pixel = (height() > 0) + ? (2.0f * half_h_world / float(height())) + : 0.0f; + + const QVector3D shift = -right * (float(dx) * pan_per_pixel) + + up * (float(dy) * pan_per_pixel); + camera_target_[0] += shift.x(); + camera_target_[1] += shift.y(); + camera_target_[2] += shift.z(); + requestUpdate(); + } +} + +void WgpuViewportWindow::wheelEvent(QWheelEvent* event) { + // 120 = one notch on a typical mouse. Each notch zooms ~12% in/out; + // sign matches conventional "wheel up = zoom in". + const float notches = float(event->angleDelta().y()) / 120.0f; + const float factor = std::pow(0.9f, notches); + camera_distance_ = std::max(0.01f, camera_distance_ * factor); + requestUpdate(); +} + void WgpuViewportWindow::shutdown() { // Release per-model buffers before the device they were created from. for (auto& [mid, m] : models_gpu_) releaseWgpuModelGpuData(m); diff --git a/src/ifcviewer-wgpu/WgpuViewportWindow.h b/src/ifcviewer-wgpu/WgpuViewportWindow.h index c686f4eb9a..488b9d64ff 100644 --- a/src/ifcviewer-wgpu/WgpuViewportWindow.h +++ b/src/ifcviewer-wgpu/WgpuViewportWindow.h @@ -22,6 +22,7 @@ #include #include +#include #include #include @@ -86,6 +87,10 @@ protected: void exposeEvent(QExposeEvent* event) override; void resizeEvent(QResizeEvent* event) override; bool event(QEvent* event) override; + void mousePressEvent(QMouseEvent* event) override; + void mouseReleaseEvent(QMouseEvent* event) override; + void mouseMoveEvent(QMouseEvent* event) override; + void wheelEvent(QWheelEvent* event) override; private: bool initWgpu(); @@ -164,6 +169,13 @@ private: // Pending one-shot screenshot, captured at the end of the next render(). QString pending_screenshot_path_; bool pending_screenshot_quit_ = false; + + // Mouse navigation state. LMB drag orbits, MMB drag pans, wheel zooms. + // No Blender/Maya preset selection yet — that arrives with the AppSettings + // port (post-task-12). Selection is unbound (no pick pass), so LMB is + // safe to dedicate to orbit for now. + Qt::MouseButton nav_active_button_ = Qt::NoButton; + QPoint nav_last_pos_; }; #endif // WGPUVIEWPORTWINDOW_H