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();