ifcviewer: move camera mutators + AABB helpers into ViewportCore (#84-i)

Move the cluster of camera-state mutators + per-object AABB helpers
now that the camera fields all live in ViewportCore. CameraState
struct is canonical in core; ViewportWindow keeps a `using` alias
so bonsai's HomeView round-trip (Commands.cpp setHome / restoreHome)
compiles unchanged.

Moved:
  void viewAll()
  void setCamera(...) — pitch + distance clamping included
  void setStandardView(yaw, pitch) — bypasses clamp for ±90°
  void toggleProjection()
  std::string cameraString() const
  CameraState cameraState() const
  void frameAabb(mn, mx, padding)
  bool computeObjectAabb(id, float[3], float[3]) const
  bool computeObjectAabb(id, Eigen::Vector3f&, Eigen::Vector3f&) const

ViewportWindow keeps thin forwarders for the public ones (bonsai
calls them). setCamera additionally flips initial_view_applied_
on the VW side — the auto-viewAll suppression flag isn't in core
yet because the trigger for auto-viewAll lives in the still-in-VW
applyCachedModel path.

The isExposed()+requestUpdate() Qt pattern inside the moved bodies
becomes host_->requestFrame(); two viewAll/toggleProjection diagnostic
prints become fprintf since Log::info() doesn't reach into core.cpp
through the Qt logging surface.

Builds: desktop / bonsai / web all green. Tests 100/100.
This commit is contained in:
Dion Moult
2026-06-05 15:27:39 +10:00
parent 14e7c9fc42
commit 707bb8f5d4
4 changed files with 189 additions and 133 deletions
+129
View File
@@ -21,6 +21,7 @@
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <limits>
#include <vector>
@@ -360,3 +361,131 @@ void ViewportCore::composeInstanceFromPlacement(InstanceCpu& inst,
}
}
}
// ---- Camera mutators ------------------------------------------------------
void ViewportCore::frameAabb(const float mn[3], const float mx[3],
float padding) {
constexpr float kDeg2Rad = float(M_PI) / 180.0f;
const float cx = 0.5f * (mn[0] + mx[0]);
const float cy = 0.5f * (mn[1] + mx[1]);
const float cz = 0.5f * (mn[2] + mx[2]);
camera_target_[0] = cx;
camera_target_[1] = cy;
camera_target_[2] = cz;
const float dx = mx[0] - mn[0];
const float dy = mx[1] - mn[1];
const float dz = mx[2] - mn[2];
const float radius = 0.5f * std::sqrt(dx*dx + dy*dy + dz*dz);
if (radius > 1e-4f) {
const float fovy_rad = camera_fov_y_deg_ * kDeg2Rad;
const float tan_half = std::tan(fovy_rad * 0.5f);
if (tan_half > 1e-6f) {
const int h = std::max(configured_h_, 1);
const float aspect = float(std::max(configured_w_, 1)) / float(h);
const float min_aspect = aspect < 1.0f ? aspect : 1.0f;
camera_distance_ = std::max(0.1f, (radius / (tan_half * min_aspect)) * padding);
}
}
host_->requestFrame();
}
void ViewportCore::viewAll() {
float mn[3], mx[3];
if (!computeSceneAabb(mn, mx)) return;
// Same math as GL's frameAabb(mn, mx, 1.10): target at centroid,
// distance pulls the bounding sphere just inside the tighter of
// horizontal/vertical FOV. 1.10 padding matches GL viewAll.
frameAabb(mn, mx, 1.10f);
const float cx = 0.5f * (mn[0] + mx[0]);
const float cy = 0.5f * (mn[1] + mx[1]);
const float cz = 0.5f * (mn[2] + mx[2]);
const float dx = mx[0] - mn[0];
const float dy = mx[1] - mn[1];
const float dz = mx[2] - mn[2];
const float radius = 0.5f * std::sqrt(dx*dx + dy*dy + dz*dz);
std::fprintf(stderr,
"[info] [wgpu] viewAll target=(%g, %g, %g) distance=%g (scene radius=%g)\n",
cx, cy, cz, camera_distance_, radius);
}
void ViewportCore::setCamera(float tx, float ty, float tz,
float dist, float yaw_deg, float pitch_deg) {
camera_target_[0] = tx;
camera_target_[1] = ty;
camera_target_[2] = tz;
camera_distance_ = std::max(0.01f, dist);
camera_yaw_deg_ = yaw_deg;
// Mirrors GL clamp — keep pitch just shy of the pole so orbit math
// doesn't degenerate. The standard-view top/bottom hotkeys go through
// setStandardView, which bypasses the clamp on purpose.
camera_pitch_deg_ = std::clamp(pitch_deg, -89.9f, 89.9f);
host_->requestFrame();
}
void ViewportCore::setStandardView(float yaw_deg, float pitch_deg) {
// Bypass the orbit-pitch clamp so top/bottom land exactly at ±90°.
// buildViewProj picks the up vector based on |pitch| so lookAt
// stays well-conditioned at the poles.
camera_yaw_deg_ = yaw_deg;
camera_pitch_deg_ = pitch_deg;
host_->requestFrame();
}
void ViewportCore::toggleProjection() {
projection_ortho_ = !projection_ortho_;
std::fprintf(stderr, "[info] [wgpu] projection: %s\n",
projection_ortho_ ? "ortho" : "perspective");
host_->requestFrame();
}
std::string ViewportCore::cameraString() const {
char buf[128];
std::snprintf(buf, sizeof(buf), "%.4f,%.4f,%.4f,%.4f,%.2f,%.2f",
camera_target_[0], camera_target_[1], camera_target_[2],
camera_distance_, camera_yaw_deg_, camera_pitch_deg_);
return std::string(buf);
}
ViewportCore::CameraState ViewportCore::cameraState() const {
CameraState s;
s.target = Eigen::Vector3f(camera_target_[0], camera_target_[1], camera_target_[2]);
s.distance = camera_distance_;
s.yaw = camera_yaw_deg_;
s.pitch = camera_pitch_deg_;
return s;
}
bool ViewportCore::computeObjectAabb(uint32_t object_id,
float mn[3], float mx[3]) const {
bool any = false;
for (int i = 0; i < 3; ++i) {
mn[i] = std::numeric_limits<float>::infinity();
mx[i] = -std::numeric_limits<float>::infinity();
}
for (const auto& [mid, m] : models_gpu_) {
for (const auto& inst : m.instances) {
if (inst.object_id != object_id) continue;
for (int i = 0; i < 3; ++i) {
mn[i] = std::min(mn[i], inst.world_aabb_min[i]);
mx[i] = std::max(mx[i], inst.world_aabb_max[i]);
}
any = true;
}
}
return any;
}
bool ViewportCore::computeObjectAabb(uint32_t object_id,
Eigen::Vector3f& mn,
Eigen::Vector3f& mx) const {
float fmin[3], fmax[3];
if (!computeObjectAabb(object_id, fmin, fmax)) return false;
mn = Eigen::Vector3f(fmin[0], fmin[1], fmin[2]);
mx = Eigen::Vector3f(fmax[0], fmax[1], fmax[2]);
return true;
}
+34
View File
@@ -38,6 +38,7 @@
#include <Eigen/Dense>
#include <cstdint>
#include <string>
#include <unordered_map>
#include "BufferPool.h"
@@ -124,6 +125,39 @@ public:
float chunkScreenAreaPx(const ModelGpuData::Chunk& c,
const Eigen::Matrix4f& vp_mat) const;
// Camera state snapshot for save-view / restore-view round-trips.
// Same shape as ViewportWindow::CameraState (kept as a `using` alias
// there) so bonsai's HomeView code keeps working.
struct CameraState {
Eigen::Vector3f target = Eigen::Vector3f::Zero();
float distance = 50.0f;
float yaw = 45.0f;
float pitch = 30.0f;
};
// ---- Camera mutators / getters ------------------------------------------
void viewAll();
void setCamera(float tx, float ty, float tz,
float dist, float yaw_deg, float pitch_deg);
void setStandardView(float yaw_deg, float pitch_deg);
void toggleProjection();
bool projectionOrtho() const { return projection_ortho_; }
std::string cameraString() const;
CameraState cameraState() const;
// Re-aim the orbit camera so [mn, mx] fits the view with `padding`
// headroom (1.10 typical). Used by viewAll and focusOnSelectedObject.
void frameAabb(const float mn[3], const float mx[3], float padding);
// Per-object AABB lookup. Aggregates every instance of `object_id`
// across every loaded model. Two overloads — float[3] for internal
// callers; the Eigen::Vector3f overload exists so bonsai's volume
// readout + focus paths compile unchanged.
bool computeObjectAabb(uint32_t object_id, float mn[3], float mx[3]) const;
bool computeObjectAabb(uint32_t object_id,
Eigen::Vector3f& mn, Eigen::Vector3f& mx) const;
// Friend access for ViewportWindow's reference proxies. As each
// render method moves into ViewportCore it stops needing these
// (it touches the fields directly); once everything has migrated
+21 -125
View File
@@ -3504,13 +3504,9 @@ void ViewportWindow::invertElementVisibility() {
if (isExposed()) requestUpdate();
}
// cameraState moved to ViewportCore (#84-i).
ViewportWindow::CameraState ViewportWindow::cameraState() const {
return CameraState{
Eigen::Vector3f(camera_target_[0], camera_target_[1], camera_target_[2]),
camera_distance_,
camera_yaw_deg_,
camera_pitch_deg_,
};
return core_.cameraState();
}
void ViewportWindow::setToolMode(ToolMode m) {
@@ -6695,112 +6691,28 @@ void ViewportWindow::updateFrameUniforms() {
// computeSceneAabb moved to ViewportCore (#84-h).
// setCamera body moved to ViewportCore (#84-i). VW keeps the wrapper
// because the auto-viewAll suppression flag (initial_view_applied_)
// still lives on the Qt-bound side — it's the first-model-loaded
// hook that ViewportCore doesn't own yet.
void ViewportWindow::setCamera(float tx, float ty, float tz,
float dist, float yaw_deg, float pitch_deg) {
camera_target_[0] = tx;
camera_target_[1] = ty;
camera_target_[2] = tz;
camera_distance_ = std::max(0.01f, dist);
camera_yaw_deg_ = yaw_deg;
camera_pitch_deg_ = std::clamp(pitch_deg, -89.9f, 89.9f);
// Suppress the auto-viewAll on the first model load so the script-set
// camera survives. Manual viewAll() calls after this still work.
float dist, float yaw_deg, float pitch_deg) {
core_.setCamera(tx, ty, tz, dist, yaw_deg, pitch_deg);
initial_view_applied_ = true;
if (isExposed()) requestUpdate();
}
void ViewportWindow::viewAll() {
float mn[3], mx[3];
if (!core_.computeSceneAabb(mn, mx)) return;
// viewAll / frameAabb / computeObjectAabb moved to ViewportCore (#84-i).
// Frame the union AABB with the same math as GL's frameAabb(mn, mx, 1.10):
// target at centroid, distance pulls the bounding sphere just inside the
// tighter of the horizontal/vertical FOV. Padding 1.10 matches GL viewAll.
const float cx = 0.5f * (mn[0] + mx[0]);
const float cy = 0.5f * (mn[1] + mx[1]);
const float cz = 0.5f * (mn[2] + mx[2]);
camera_target_[0] = cx;
camera_target_[1] = cy;
camera_target_[2] = cz;
const float dx = mx[0] - mn[0];
const float dy = mx[1] - mn[1];
const float dz = mx[2] - mn[2];
const float radius = 0.5f * std::sqrt(dx*dx + dy*dy + dz*dz);
if (radius > 1e-4f) {
const float fovy_rad = qDegreesToRadians(camera_fov_y_deg_);
const float tan_half = std::tan(fovy_rad * 0.5f);
if (tan_half > 1e-6f) {
const int h = std::max(configured_h_, 1);
const float aspect = float(std::max(configured_w_, 1)) / float(h);
const float min_aspect = aspect < 1.0f ? aspect : 1.0f;
camera_distance_ = std::max(0.1f, (radius / (tan_half * min_aspect)) * 1.10f);
}
}
Log::info().noquote().nospace()
<< "[wgpu] viewAll target=(" << cx << ", " << cy << ", " << cz << ")"
<< " distance=" << camera_distance_
<< " (scene radius=" << radius << ")";
if (isExposed()) requestUpdate();
void ViewportWindow::viewAll() { core_.viewAll(); }
void ViewportWindow::frameAabb(const float mn[3], const float mx[3], float padding) {
core_.frameAabb(mn, mx, padding);
}
void ViewportWindow::frameAabb(const float mn[3], const float mx[3],
float padding) {
const float cx = 0.5f * (mn[0] + mx[0]);
const float cy = 0.5f * (mn[1] + mx[1]);
const float cz = 0.5f * (mn[2] + mx[2]);
camera_target_[0] = cx;
camera_target_[1] = cy;
camera_target_[2] = cz;
const float dx = mx[0] - mn[0];
const float dy = mx[1] - mn[1];
const float dz = mx[2] - mn[2];
const float radius = 0.5f * std::sqrt(dx*dx + dy*dy + dz*dz);
if (radius > 1e-4f) {
const float fovy_rad = qDegreesToRadians(camera_fov_y_deg_);
const float tan_half = std::tan(fovy_rad * 0.5f);
if (tan_half > 1e-6f) {
const int h = std::max(configured_h_, 1);
const float aspect = float(std::max(configured_w_, 1)) / float(h);
const float min_aspect = aspect < 1.0f ? aspect : 1.0f;
camera_distance_ = std::max(0.1f, (radius / (tan_half * min_aspect)) * padding);
}
}
if (isExposed()) requestUpdate();
bool ViewportWindow::computeObjectAabb(uint32_t id, float mn[3], float mx[3]) const {
return core_.computeObjectAabb(id, mn, mx);
}
bool ViewportWindow::computeObjectAabb(uint32_t object_id,
Eigen::Vector3f& mn, Eigen::Vector3f& mx) const {
float fmin[3], fmax[3];
if (!computeObjectAabb(object_id, fmin, fmax)) return false;
mn = Eigen::Vector3f(fmin[0], fmin[1], fmin[2]);
mx = Eigen::Vector3f(fmax[0], fmax[1], fmax[2]);
return true;
}
bool ViewportWindow::computeObjectAabb(uint32_t object_id,
float mn[3], float mx[3]) const {
bool any = false;
for (int i = 0; i < 3; ++i) {
mn[i] = std::numeric_limits<float>::infinity();
mx[i] = -std::numeric_limits<float>::infinity();
}
for (const auto& [mid, m] : models_gpu_) {
for (const auto& inst : m.instances) {
if (inst.object_id != object_id) continue;
for (int i = 0; i < 3; ++i) {
mn[i] = std::min(mn[i], inst.world_aabb_min[i]);
mx[i] = std::max(mx[i], inst.world_aabb_max[i]);
}
any = true;
}
}
return any;
bool ViewportWindow::computeObjectAabb(uint32_t id,
Eigen::Vector3f& mn, Eigen::Vector3f& mx) const {
return core_.computeObjectAabb(id, mn, mx);
}
void ViewportWindow::focusOnSelectedObject() {
@@ -6832,28 +6744,12 @@ void ViewportWindow::focusOnSelectedObject() {
frameAabb(lo, hi, 1.30f);
}
// setStandardView / toggleProjection / cameraString moved to ViewportCore (#84-i).
void ViewportWindow::setStandardView(float yaw_deg, float pitch_deg) {
// Bypasses the orbit-pitch clamp so top/bottom land exactly at ±90°.
// buildViewProj() picks the up vector based on |pitch| so lookAt stays
// well-conditioned at the poles.
camera_yaw_deg_ = yaw_deg;
camera_pitch_deg_ = pitch_deg;
if (isExposed()) requestUpdate();
}
void ViewportWindow::toggleProjection() {
projection_ortho_ = !projection_ortho_;
Log::info() << "[wgpu] projection:" << (projection_ortho_ ? "ortho" : "perspective");
if (isExposed()) requestUpdate();
}
std::string ViewportWindow::cameraString() const {
char buf[128];
std::snprintf(buf, sizeof(buf), "%.4f,%.4f,%.4f,%.4f,%.2f,%.2f",
camera_target_[0], camera_target_[1], camera_target_[2],
camera_distance_, camera_yaw_deg_, camera_pitch_deg_);
return std::string(buf);
core_.setStandardView(yaw_deg, pitch_deg);
}
void ViewportWindow::toggleProjection() { core_.toggleProjection(); }
std::string ViewportWindow::cameraString() const { return core_.cameraString(); }
void ViewportWindow::enterFpsMode() {
if (fps_mode_) return;
+5 -8
View File
@@ -174,14 +174,11 @@ public:
bool projectionOrtho() const { return projection_ortho_; }
std::string cameraString() const;
// Snapshot of the orbit camera. Mirrors GL ViewportWindow::CameraState
// so bonsai's "save view" / "restore view" commands port unchanged.
struct CameraState {
Eigen::Vector3f target;
float distance;
float yaw; // degrees
float pitch; // degrees
};
// Snapshot of the orbit camera. Canonical struct now lives in
// ViewportCore so the camera-mutator path stays Qt-free (#84-i);
// the alias keeps bonsai's "save view" / "restore view" callers
// working unchanged.
using CameraState = ViewportCore::CameraState;
CameraState cameraState() const;
// Tool toggles: flip between NoTool and the named tool. Wrappers