From a06e5ecdf53ec3e7d68c7e586514516020092a75 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 30 Apr 2026 11:34:35 +1000 Subject: [PATCH] ifcviewer: add Focus-on-Object and View-All camera shortcuts F (no modifier) re-aims the orbit camera at the selected object's world AABB centroid and dollies camera_distance_ so the bounding sphere fits the current viewport. Home does the same for the union of all finalized models. Both preserve yaw/pitch so the user keeps their orientation; both no-op in FPS mode. Scene AABB prefers the per-model BVH root when available and falls back to walking InstanceCpu world AABBs. Object AABB unions every matching instance. Distance accounts for portrait windows by using the tighter of the horizontal and vertical FOV constraints. Co-Authored-By: Claude Opus 4.7 --- src/ifcviewer/ViewportWindow.cpp | 116 +++++++++++++++++++++++++++++++ src/ifcviewer/ViewportWindow.h | 19 +++++ 2 files changed, 135 insertions(+) diff --git a/src/ifcviewer/ViewportWindow.cpp b/src/ifcviewer/ViewportWindow.cpp index df6bbe96a4..0dfc2997a0 100644 --- a/src/ifcviewer/ViewportWindow.cpp +++ b/src/ifcviewer/ViewportWindow.cpp @@ -1230,6 +1230,108 @@ void ViewportWindow::setCamera(float tx, float ty, float tz, requestUpdate(); } +bool ViewportWindow::computeObjectAabb(uint32_t object_id, + QVector3D& mn, QVector3D& mx) const { + if (object_id == 0) return false; + bool found = false; + QVector3D lo( std::numeric_limits::max(), + std::numeric_limits::max(), + std::numeric_limits::max()); + QVector3D hi(-std::numeric_limits::max(), + -std::numeric_limits::max(), + -std::numeric_limits::max()); + for (const auto& [mid, m] : models_gpu_) { + if (!m.finalized || m.hidden) continue; + for (const InstanceCpu& inst : m.instances) { + if (inst.object_id != object_id) continue; + for (int a = 0; a < 3; ++a) { + if (inst.world_aabb_min[a] < lo[a]) lo[a] = inst.world_aabb_min[a]; + if (inst.world_aabb_max[a] > hi[a]) hi[a] = inst.world_aabb_max[a]; + } + found = true; + } + } + if (found) { mn = lo; mx = hi; } + return found; +} + +bool ViewportWindow::computeSceneAabb(QVector3D& mn, QVector3D& mx) const { + bool found = false; + QVector3D lo( std::numeric_limits::max(), + std::numeric_limits::max(), + std::numeric_limits::max()); + QVector3D hi(-std::numeric_limits::max(), + -std::numeric_limits::max(), + -std::numeric_limits::max()); + for (const auto& [mid, m] : models_gpu_) { + if (!m.finalized || m.hidden) continue; + if (!m.bvh.nodes.empty()) { + const BvhNode& root = m.bvh.nodes[0]; + for (int a = 0; a < 3; ++a) { + if (root.aabb_min[a] < lo[a]) lo[a] = root.aabb_min[a]; + if (root.aabb_max[a] > hi[a]) hi[a] = root.aabb_max[a]; + } + found = true; + } else { + for (const InstanceCpu& inst : m.instances) { + for (int a = 0; a < 3; ++a) { + if (inst.world_aabb_min[a] < lo[a]) lo[a] = inst.world_aabb_min[a]; + if (inst.world_aabb_max[a] > hi[a]) hi[a] = inst.world_aabb_max[a]; + } + found = true; + } + } + } + if (found) { mn = lo; mx = hi; } + return found; +} + +void ViewportWindow::frameAabb(const QVector3D& mn, const QVector3D& mx, + float padding) { + const QVector3D centroid = (mn + mx) * 0.5f; + const float radius = ((mx - mn).length() * 0.5f); + + // Empty / point AABB: keep the existing distance so we just recenter. + float new_distance = camera_distance_; + if (radius > 1e-4f) { + const float fovy_rad = qDegreesToRadians(camera_fov_y_deg_); + const float tan_half = tanf(fovy_rad * 0.5f); + // tan_half == 0 is impossible at fov 45°, but guard anyway. + if (tan_half > 1e-6f) { + const int h = qMax(height(), 1); + const float aspect = float(qMax(width(), 1)) / float(h); + // Use the tighter axis: portrait windows need a larger pull-back. + const float min_aspect = aspect < 1.0f ? aspect : 1.0f; + new_distance = (radius / (tan_half * min_aspect)) * padding; + } + } + + camera_target_ = centroid; + camera_distance_ = qMax(0.1f, new_distance); + have_cached_cull_ = false; + requestUpdate(); +} + +void ViewportWindow::focusOnSelectedObject() { + if (camera_mode_ == CameraMode::Fps) return; + QVector3D mn, mx; + if (!computeObjectAabb(selected_object_id_, mn, mx)) { + qDebug("Focus: no object selected or no AABB available"); + return; + } + frameAabb(mn, mx, 1.30f); // a bit of headroom around small objects +} + +void ViewportWindow::viewAll() { + if (camera_mode_ == CameraMode::Fps) return; + QVector3D mn, mx; + if (!computeSceneAabb(mn, mx)) { + qDebug("View All: scene is empty"); + return; + } + frameAabb(mn, mx, 1.10f); +} + void ViewportWindow::setBenchmarkFrames(int n) { benchmark_total_ = n; benchmark_count_ = 0; @@ -1296,6 +1398,20 @@ void ViewportWindow::keyPressEvent(QKeyEvent* event) { qDebug("--camera %s", qPrintable(cameraString())); return; } + + // Plain F (no modifiers): focus camera on the currently selected object. + // Shift+F is FPS-mode toggle and was handled above. + if (key == Qt::Key_F + && event->modifiers() == Qt::NoModifier + && !event->isAutoRepeat()) { + focusOnSelectedObject(); + return; + } + // Home: frame the entire scene. + if (key == Qt::Key_Home && !event->isAutoRepeat()) { + viewAll(); + return; + } QWindow::keyPressEvent(event); } diff --git a/src/ifcviewer/ViewportWindow.h b/src/ifcviewer/ViewportWindow.h index 5e2e7ad92b..41bd01b58d 100644 --- a/src/ifcviewer/ViewportWindow.h +++ b/src/ifcviewer/ViewportWindow.h @@ -176,6 +176,14 @@ public: void setBenchmarkFrames(int n); QString cameraString() const; + // Move camera_target_ to the selected object's world-AABB centroid and + // dolly camera_distance_ so the object's bounding sphere fits the + // current viewport. Yaw/pitch are preserved. No-op if no object is + // selected or its AABB is unknown. + void focusOnSelectedObject(); + // Frame the union of all finalized models. No-op if the scene is empty. + void viewAll(); + struct CameraState { QVector3D target; float distance; @@ -237,6 +245,17 @@ private: void renderAxisGizmo(); void renderPivotIndicator(); void updateCamera(); + + // Geometry queries used by focusOnSelectedObject() / viewAll(). Both + // return false when nothing matched (caller should leave the camera + // alone). Bounds are world-space AABBs. + bool computeObjectAabb(uint32_t object_id, QVector3D& mn, QVector3D& mx) const; + bool computeSceneAabb(QVector3D& mn, QVector3D& mx) const; + // Re-aim the orbit camera so the bounding sphere of [mn, mx] just fits + // vertically and horizontally within the current FOV, with `padding` + // headroom (1.0 = tight). Yaw/pitch are preserved; only target and + // distance change. + void frameAabb(const QVector3D& mn, const QVector3D& mx, float padding); void buildShaders(); void buildAxisGizmo(); void buildPivotIndicator();