diff --git a/src/ifcviewer/ViewportCore.cpp b/src/ifcviewer/ViewportCore.cpp index 4affd7422a..aa9b1dc99c 100644 --- a/src/ifcviewer/ViewportCore.cpp +++ b/src/ifcviewer/ViewportCore.cpp @@ -445,6 +445,53 @@ void ViewportCore::setStandardView(float yaw_deg, float pitch_deg) { host_->requestFrame(); } +void ViewportCore::orbitBy(float dx_px, float dy_px) { + // 0.4 deg/px matches the GL viewport. pitch is clamped just shy of + // the pole so orbitEye() stays well-conditioned. + camera_yaw_deg_ -= dx_px * 0.4f; + camera_pitch_deg_ += dy_px * 0.4f; + camera_pitch_deg_ = std::clamp(camera_pitch_deg_, -89.9f, 89.9f); + host_->requestFrame(); +} + +void ViewportCore::panBy(float dx_px, float dy_px, int viewport_height_px) { + constexpr float kDeg2Rad = float(M_PI) / 180.0f; + + // Pan in the camera's screen-space plane. Within 1° of straight + // up/down the world-Z up-reference degenerates (cross with forward + // is the zero vector → NaN), so switch to world-Y up — matches the + // up-vector switch in buildViewProj so top/bottom views still pan. + const Eigen::Vector3f target(camera_target_[0], camera_target_[1], camera_target_[2]); + const Eigen::Vector3f eye = orbitEye(camera_target_, camera_distance_, + camera_yaw_deg_, camera_pitch_deg_); + const Eigen::Vector3f fwd = (target - eye).normalized(); + const Eigen::Vector3f world_up = (std::abs(camera_pitch_deg_) >= 89.0f) + ? Eigen::Vector3f(0.0f, 1.0f, 0.0f) + : Eigen::Vector3f(0.0f, 0.0f, 1.0f); + const Eigen::Vector3f right = fwd.cross(world_up).normalized(); + const Eigen::Vector3f up = right.cross(fwd).normalized(); + + const float half_h_world = camera_distance_ + * std::tan(camera_fov_y_deg_ * 0.5f * kDeg2Rad); + const float pan_per_pixel = (viewport_height_px > 0) + ? (2.0f * half_h_world / float(viewport_height_px)) + : 0.0f; + + const Eigen::Vector3f shift = -right * (dx_px * pan_per_pixel) + + up * (dy_px * pan_per_pixel); + camera_target_[0] += shift.x(); + camera_target_[1] += shift.y(); + camera_target_[2] += shift.z(); + host_->requestFrame(); +} + +void ViewportCore::dollyBy(float notches) { + // Each notch zooms ~10% in/out; sign matches "wheel up = in". + const float factor = std::pow(0.9f, notches); + camera_distance_ = std::max(0.01f, camera_distance_ * factor); + host_->requestFrame(); +} + void ViewportCore::toggleProjection() { projection_ortho_ = !projection_ortho_; std::fprintf(stderr, "[info] [wgpu] projection: %s\n", diff --git a/src/ifcviewer/ViewportCore.h b/src/ifcviewer/ViewportCore.h index 373b5540b4..903a8c27b7 100644 --- a/src/ifcviewer/ViewportCore.h +++ b/src/ifcviewer/ViewportCore.h @@ -184,6 +184,26 @@ public: void setCamera(float tx, float ty, float tz, float dist, float yaw_deg, float pitch_deg); void setStandardView(float yaw_deg, float pitch_deg); + + // ---- Incremental orbit navigation --------------------------------------- + // + // Pixel-delta camera moves, shared by every host (Qt desktop + web). + // Hosts translate raw pointer/wheel events into these calls and own + // their own UI concerns (drag promotion, pivot indicator, cursor + // capture); the orbit math lives here so it can't drift between + // platforms. Each schedules a frame via the host. + // + // orbitBy: drag-right yaws the world right (yaw -= dx), drag-down + // tilts the camera up (pitch += dy). 0.4 deg/px matches GL. + // panBy: shifts the target in the camera's screen plane; world + // units/pixel track the frustum height at the pivot so the + // feel is zoom-independent. Needs the viewport height. + // dollyBy: each wheel notch zooms ~10% (distance *= 0.9^notches); + // positive notches zoom in. + void orbitBy(float dx_px, float dy_px); + void panBy(float dx_px, float dy_px, int viewport_height_px); + void dollyBy(float notches); + void toggleProjection(); bool projectionOrtho() const { return projection_ortho_; } std::string cameraString() const;