diff --git a/src/bonsaiviewer/modules/viewport/View.cpp b/src/bonsaiviewer/modules/viewport/View.cpp index e5eaa02723..cf9140bf31 100644 --- a/src/bonsaiviewer/modules/viewport/View.cpp +++ b/src/bonsaiviewer/modules/viewport/View.cpp @@ -45,11 +45,14 @@ ViewportView::ViewportView(bonsaiviewer::SessionState* session_state, , length_measurement_(std::make_unique()) { auto& settings = bonsaiviewer::ViewerSettings::instance(); - connect(&settings, &bonsaiviewer::ViewerSettings::themeChanged, this, [this]() { - viewport_->setBackgroundColor( - QColor(bonsaiviewer::ViewerSettings::instance().color("viewport_background"))); - }); - viewport_->setBackgroundColor(QColor(settings.color("viewport_background"))); + auto setBg = [this](const QString& name) { + const QColor c(bonsaiviewer::ViewerSettings::instance().color(name)); + viewport_->setBackgroundColor(float(c.redF()), float(c.greenF()), + float(c.blueF()), float(c.alphaF())); + }; + connect(&settings, &bonsaiviewer::ViewerSettings::themeChanged, this, + [setBg]() { setBg("viewport_background"); }); + setBg("viewport_background"); connect(session_state_, &SessionState::projectReset, this, &ViewportView::refresh); connect(session_state_, &SessionState::projectOpened, this, [this](const QString&) { refresh(); }); diff --git a/src/ifcviewer/CMakeLists.txt b/src/ifcviewer/CMakeLists.txt index 71b8436771..9d7bbffd90 100644 --- a/src/ifcviewer/CMakeLists.txt +++ b/src/ifcviewer/CMakeLists.txt @@ -155,6 +155,7 @@ set(IFCVIEWER_CORE_HEADERS InstancedGeometry.h Log.h LodBuilder.h + Stopwatch.h ModelGpuData.h SelectionState.h SidecarCache.h diff --git a/src/ifcviewer/OverlayRenderer.cpp b/src/ifcviewer/OverlayRenderer.cpp index 1b3e645272..0db64cf6a4 100644 --- a/src/ifcviewer/OverlayRenderer.cpp +++ b/src/ifcviewer/OverlayRenderer.cpp @@ -1186,8 +1186,8 @@ bool OverlayRenderer::buildMarquee() { void OverlayRenderer::encodeMarquee(WGPUCommandEncoder enc, WGPUTextureView surface_view, const OverlayFrame& f, - QPoint start_logical_px, - QPoint current_logical_px, + Eigen::Vector2i start_logical_px, + Eigen::Vector2i current_logical_px, bool active) { if (!marquee_pipeline_ || !surface_view) return; if (!active) return; diff --git a/src/ifcviewer/OverlayRenderer.h b/src/ifcviewer/OverlayRenderer.h index e5866ee2a5..cb9e94f73f 100644 --- a/src/ifcviewer/OverlayRenderer.h +++ b/src/ifcviewer/OverlayRenderer.h @@ -21,7 +21,6 @@ #define WGPUOVERLAYRENDERER_H #include -#include #include #include @@ -189,8 +188,8 @@ public: void encodeMarquee(WGPUCommandEncoder enc, WGPUTextureView surface_view, const OverlayFrame& f, - QPoint start_logical_px, - QPoint current_logical_px, + Eigen::Vector2i start_logical_px, + Eigen::Vector2i current_logical_px, bool active); // Shared with the viewport's main FrameUniforms: same cap so the diff --git a/src/ifcviewer/Stopwatch.h b/src/ifcviewer/Stopwatch.h new file mode 100644 index 0000000000..b249736ef5 --- /dev/null +++ b/src/ifcviewer/Stopwatch.h @@ -0,0 +1,62 @@ +/******************************************************************************** + * * + * This file is part of IfcOpenShell. * + * * + * IfcOpenShell is free software: you can redistribute it and/or modify * + * it under the terms of the Lesser GNU General Public License as published by * + * the Free Software Foundation, either version 3.0 of the License, or * + * (at your option) any later version. * + * * + * IfcOpenShell is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * Lesser GNU General Public License for more details. * + * * + * You should have received a copy of the Lesser GNU General Public License * + * along with this program. If not, see . * + * * + ********************************************************************************/ + +#ifndef IFCVIEWER_STOPWATCH_H +#define IFCVIEWER_STOPWATCH_H + +// Qt-free QElapsedTimer replacement: a thin wrapper over +// std::chrono::steady_clock that exposes the handful of methods our code +// actually uses (start, restart, elapsed-as-ms, nsecsElapsed, isValid). +// Same call-site API as QElapsedTimer so the existing diagnostics keep +// reading the same way after the type swap. + +#include +#include + +class Stopwatch { +public: + using Clock = std::chrono::steady_clock; + using TimePoint = Clock::time_point; + + Stopwatch() = default; + + bool isValid() const { return started_; } + void start() { t0_ = Clock::now(); started_ = true; } + void restart() { start(); } + void invalidate() { started_ = false; } + + // Elapsed milliseconds since start(); matches QElapsedTimer::elapsed(). + int64_t elapsed() const { + if (!started_) return 0; + return std::chrono::duration_cast( + Clock::now() - t0_).count(); + } + // Nanoseconds since start; matches QElapsedTimer::nsecsElapsed(). + int64_t nsecsElapsed() const { + if (!started_) return 0; + return std::chrono::duration_cast( + Clock::now() - t0_).count(); + } + +private: + TimePoint t0_{}; + bool started_ = false; +}; + +#endif // IFCVIEWER_STOPWATCH_H diff --git a/src/ifcviewer/ViewportWindow.cpp b/src/ifcviewer/ViewportWindow.cpp index 8bd46e342d..311ffdf51b 100644 --- a/src/ifcviewer/ViewportWindow.cpp +++ b/src/ifcviewer/ViewportWindow.cpp @@ -32,7 +32,6 @@ #include #include #include -#include #include #include #include @@ -115,6 +114,14 @@ static double computeMeshLocalVolumeQuantised( // AND by pickMeshLocalAt to refine the AABB-coarse surface hit into a // real triangle hit — see pickMeshLocalAt's refinement block. +// Convert Qt's pixel-coord QPoint (event payload) to the Eigen::Vector2i +// we store in member fields. The cast is mechanical but isolating it as +// a helper keeps every event-handler site one line shorter. +#include +static inline Eigen::Vector2i toV2i(const QPoint& p) { + return Eigen::Vector2i(p.x(), p.y()); +} + // Slab method ray-AABB. inv_d is precomputed 1/dir per axis. static bool rayAabbSlab(const float ro[3], const float inv_d[3], @@ -680,8 +687,8 @@ void ViewportWindow::onToolBackspacePressed() { emit toolBackspacePressed(); } -void ViewportWindow::setBackgroundColor(const QColor& color) { - background_color_ = color; +void ViewportWindow::setBackgroundColor(float r, float g, float b, float a) { + background_color_ = {r, g, b, a}; if (isExposed()) requestUpdate(); } @@ -4625,7 +4632,7 @@ void ViewportWindow::cullModelCpuUpload(ModelGpuData& m) { void ViewportWindow::render() { // Time the whole render() body (cull + encode + present) for the // benchmark stats. Started before any wgpu work so cull is included. - QElapsedTimer frame_timer; + Stopwatch frame_timer; frame_timer.start(); // Advance fly-mode camera by wall-clock dt since the last frame so the @@ -4676,7 +4683,7 @@ void ViewportWindow::render() { last_visible_triangles_ = 0; last_sub_draws_ = 0; hiz_reject_count_ = 0; - QElapsedTimer cull_timer; + Stopwatch cull_timer; cull_timer.start(); Eigen::Matrix4f vp_this_frame; { @@ -4823,7 +4830,7 @@ void ViewportWindow::render() { // chunks ≈ 360 wgpu calls/frame). If upload >> compute the parallel // cull is doing its job and the bottleneck is somewhere else. const double cull_compute_ms = double(cull_timer.nsecsElapsed()) / 1e6; - QElapsedTimer upload_timer; + Stopwatch upload_timer; upload_timer.start(); for (auto& [mid, m] : models_gpu_) { if (m.hidden) continue; @@ -4848,7 +4855,7 @@ void ViewportWindow::render() { // visible into residency. Runs before draw encoding so newly-loaded // chunks render the same frame. Timed separately because synchronous // disk reads here can dwarf the cull itself on big scenes. - QElapsedTimer stream_timer; + Stopwatch stream_timer; stream_timer.start(); driveStreamingLoads(); const double stream_ms = double(stream_timer.nsecsElapsed()) / 1e6; @@ -4875,9 +4882,9 @@ void ViewportWindow::render() { color.loadOp = WGPULoadOp_Clear; color.storeOp = WGPUStoreOp_Store; color.clearValue = { - srgbToLinear(background_color_.redF()), - srgbToLinear(background_color_.greenF()), - srgbToLinear(background_color_.blueF()), + srgbToLinear(background_color_[0]), + srgbToLinear(background_color_[1]), + srgbToLinear(background_color_[2]), 1.0, }; color.depthSlice = WGPU_DEPTH_SLICE_UNDEFINED; @@ -5186,7 +5193,7 @@ void ViewportWindow::render() { // frame. Drainage happens at the top of the *next* frame via // drainHizReadbacks(), giving the GPU at least one frame of headroom. if (hiz_enabled_ && hiz_submitted_slot >= 0) { - QElapsedTimer hiz_timer; + Stopwatch hiz_timer; if (bench_total_ > 0) hiz_timer.start(); startHizMap(hiz_submitted_slot, vp_this_frame); if (bench_total_ > 0 && bench_count_ >= bench_warmup_) { @@ -7024,11 +7031,11 @@ void ViewportWindow::enterFpsMode() { if (fps_mode_) return; fps_mode_ = true; fps_keys_held_.clear(); - fps_press_center_ = QPoint(width() / 2, height() / 2); + fps_press_center_ = Eigen::Vector2i(width() / 2, height() / 2); fps_ignore_next_mouse_move_ = true; fps_last_tick_.start(); setCursor(Qt::BlankCursor); - QCursor::setPos(mapToGlobal(fps_press_center_)); + QCursor::setPos(mapToGlobal(QPoint(fps_press_center_.x(), fps_press_center_.y()))); Log::info() << "[wgpu] fly mode active — WASD/QE to move, Shift to boost, Esc to exit"; if (isExposed()) requestUpdate(); } @@ -7043,7 +7050,7 @@ void ViewportWindow::exitFpsMode() { } void ViewportWindow::fpsIntegrate() { - if (!fps_mode_ || fps_keys_held_.isEmpty()) return; + if (!fps_mode_ || fps_keys_held_.empty()) return; const qint64 elapsed_ns = fps_last_tick_.nsecsElapsed(); fps_last_tick_.restart(); @@ -7070,12 +7077,12 @@ void ViewportWindow::fpsIntegrate() { right.normalize(); Eigen::Vector3f move(0, 0, 0); - if (fps_keys_held_.contains(Qt::Key_W)) move += forward; - if (fps_keys_held_.contains(Qt::Key_S)) move -= forward; - if (fps_keys_held_.contains(Qt::Key_D)) move += right; - if (fps_keys_held_.contains(Qt::Key_A)) move -= right; - if (fps_keys_held_.contains(Qt::Key_E)) move += world_up; - if (fps_keys_held_.contains(Qt::Key_Q)) move -= world_up; + if (fps_keys_held_.count(Qt::Key_W)) move += forward; + if (fps_keys_held_.count(Qt::Key_S)) move -= forward; + if (fps_keys_held_.count(Qt::Key_D)) move += right; + if (fps_keys_held_.count(Qt::Key_A)) move -= right; + if (fps_keys_held_.count(Qt::Key_E)) move += world_up; + if (fps_keys_held_.count(Qt::Key_Q)) move -= world_up; if (move.isZero()) return; move.normalize(); @@ -7084,7 +7091,7 @@ void ViewportWindow::fpsIntegrate() { // because distance varies frame-to-frame (and worse, wheel zoom kept // changing it underneath fly mode). const float speed = fps_move_speed_ - * (fps_keys_held_.contains(Qt::Key_Shift) ? 5.0f : 1.0f); + * (fps_keys_held_.count(Qt::Key_Shift) ? 5.0f : 1.0f); const Eigen::Vector3f delta = move * (speed * dt); camera_target_[0] += delta.x(); @@ -7239,7 +7246,7 @@ void ViewportWindow::mousePressEvent(QMouseEvent* event) { } nav_active_button_ = event->button(); - nav_last_pos_ = event->position().toPoint(); + nav_last_pos_ = toV2i(event->position().toPoint()); nav_press_pos_ = nav_last_pos_; nav_dragged_ = false; @@ -7249,7 +7256,7 @@ void ViewportWindow::mousePressEvent(QMouseEvent* event) { if (section_tool_active_ && event->button() == Qt::LeftButton && event->modifiers() == Qt::NoModifier) { - const QPoint lp = event->position().toPoint(); + const Eigen::Vector2i lp = toV2i(event->position().toPoint()); const int hit = hitTestSectionGizmo(lp.x(), lp.y()); if (hit >= 0) { section_drag_active_ = true; @@ -7347,7 +7354,7 @@ void ViewportWindow::mouseReleaseEvent(QMouseEvent* event) { // route through the selection state. Shift = add, Ctrl = remove, // no modifier = replace. Empty-space click clears. if (event->button() == Qt::LeftButton && !nav_dragged_) { - const QPoint pos = event->position().toPoint(); + const Eigen::Vector2i pos = toV2i(event->position().toPoint()); const int px = int(pos.x() * devicePixelRatio()); const int py = int(pos.y() * devicePixelRatio()); @@ -7497,7 +7504,7 @@ void ViewportWindow::mouseMoveEvent(QMouseEvent* event) { // classification already declined this drag in mousePressEvent, so all // we have to do is slide the plane along its normal. if (section_drag_active_) { - const QPoint pos = event->position().toPoint(); + const Eigen::Vector2i pos = toV2i(event->position().toPoint()); updateSectionDrag(pos.x(), pos.y()); return; } @@ -7507,10 +7514,11 @@ void ViewportWindow::mouseMoveEvent(QMouseEvent* event) { // marquee triggers requestUpdate every frame the cursor moves so the // rect re-renders. if (box_select_armed_) { - const QPoint pos = event->position().toPoint(); + const Eigen::Vector2i pos = toV2i(event->position().toPoint()); box_select_current_pos_ = pos; if (!box_select_active_) { - if ((pos - box_select_start_pos_).manhattanLength() + const Eigen::Vector2i diff = pos - box_select_start_pos_; + if (std::abs(diff.x()) + std::abs(diff.y()) >= kBoxSelectThresholdPx) { box_select_active_ = true; } @@ -7531,7 +7539,7 @@ void ViewportWindow::mouseMoveEvent(QMouseEvent* event) { fps_ignore_next_mouse_move_ = false; return; } - const QPoint pos = event->position().toPoint(); + const Eigen::Vector2i pos = toV2i(event->position().toPoint()); const int dx = pos.x() - fps_press_center_.x(); const int dy = pos.y() - fps_press_center_.y(); @@ -7563,14 +7571,14 @@ void ViewportWindow::mouseMoveEvent(QMouseEvent* event) { camera_target_[2] = pinned_eye.z() - camera_distance_ * sp; fps_ignore_next_mouse_move_ = true; - QCursor::setPos(mapToGlobal(fps_press_center_)); + QCursor::setPos(mapToGlobal(QPoint(fps_press_center_.x(), fps_press_center_.y()))); requestUpdate(); return; } if (nav_active_button_ == Qt::NoButton) return; - const QPoint pos = event->position().toPoint(); + const Eigen::Vector2i pos = toV2i(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; @@ -7638,7 +7646,7 @@ void ViewportWindow::keyPressEvent(QKeyEvent* event) { case Qt::Key_W: case Qt::Key_A: case Qt::Key_S: case Qt::Key_D: case Qt::Key_Q: case Qt::Key_E: case Qt::Key_Shift: if (!event->isAutoRepeat()) { - const bool was_empty = fps_keys_held_.isEmpty(); + const bool was_empty = fps_keys_held_.empty(); fps_keys_held_.insert(key); if (was_empty) fps_last_tick_.restart(); // ALWAYS kick the render loop, not just on first key. @@ -7804,7 +7812,7 @@ void ViewportWindow::keyPressEvent(QKeyEvent* event) { void ViewportWindow::keyReleaseEvent(QKeyEvent* event) { if (fps_mode_ && !event->isAutoRepeat()) { - fps_keys_held_.remove(event->key()); + fps_keys_held_.erase(event->key()); } QWindow::keyReleaseEvent(event); } diff --git a/src/ifcviewer/ViewportWindow.h b/src/ifcviewer/ViewportWindow.h index 8c7f854549..e55eeef1bd 100644 --- a/src/ifcviewer/ViewportWindow.h +++ b/src/ifcviewer/ViewportWindow.h @@ -21,13 +21,13 @@ #define WGPUVIEWPORTWINDOW_H #include -#include -#include -#include -#include -#include #include +#include +#include + +#include "Stopwatch.h" + #include #include @@ -89,7 +89,7 @@ public: void onToolModeChanged(int tool_mode) override; void onToolBackspacePressed() override; - void setBackgroundColor(const QColor& color); + void setBackgroundColor(float r, float g, float b, float a = 1.0f); // Queue a sidecar path to be loaded after wgpu init completes. Safe to // call before the window is exposed. The path is resolved against the @@ -811,8 +811,8 @@ private: // release: plain → replace, Shift → add, Ctrl → remove. bool box_select_armed_ = false; bool box_select_active_ = false; - QPoint box_select_start_pos_; // logical px - QPoint box_select_current_pos_; // logical px + Eigen::Vector2i box_select_start_pos_; // logical px + Eigen::Vector2i box_select_current_pos_; // logical px Qt::KeyboardModifiers box_select_press_mods_ = Qt::NoModifier; static constexpr int kBoxSelectThresholdPx = 5; // R32UInt staging for the rect-pick. Sized to the largest rect we've @@ -824,7 +824,7 @@ private: // the press fall through to the orbit/pan handlers. bool section_drag_active_ = false; int section_drag_index_ = -1; - QPoint section_drag_start_mouse_; + Eigen::Vector2i section_drag_start_mouse_; Eigen::Vector3f section_drag_start_origin_; // Mirrors GL ViewportWindow::hitTestSectionGizmo: returns the index of // the plane whose arrow gizmo is within grab_px of (x, y), or -1. @@ -858,7 +858,8 @@ private: // thread per model. mutable std::atomic hiz_trace_budget_{0}; - QColor background_color_ = QColor("#202329"); + // 0x20 / 0xff ≈ 0.125, 0x23 / 0xff ≈ 0.137, 0x29 / 0xff ≈ 0.161. + Eigen::Vector4f background_color_ = {0.125f, 0.137f, 0.161f, 1.0f}; // Camera (orbit, right-handed Y-up world → wait, BIM is +Z up). // Mirrors the GL viewport's defaults; mouse navigation lands later. @@ -878,9 +879,9 @@ private: // exit via Esc (also any unrelated key click) — recenter the cursor // back at fps_press_center_ so the orbit camera resumes cleanly. bool fps_mode_ = false; - QSet fps_keys_held_; - QElapsedTimer fps_last_tick_; - QPoint fps_press_center_; + std::unordered_set fps_keys_held_; + Stopwatch fps_last_tick_; + Eigen::Vector2i fps_press_center_; bool fps_ignore_next_mouse_move_ = false; // Fly base speed in m/s at no-modifier (Shift gives a 5× boost). Default // 5.0 matches GL fps_move_speed_. Scrollwheel in fly mode adjusts this @@ -892,7 +893,7 @@ private: // print dt of each fpsIntegrate call and the prior render's elapsed // ms. Off by default (env-gated) so the normal log stays clean. bool fly_debug_ = false; - QElapsedTimer fly_render_clock_; + Stopwatch fly_render_clock_; // Click-and-track diagnostic: when a pick lands, stash the chunk // that holds the picked object. driveStreamingLoads watches for that @@ -1064,8 +1065,8 @@ private: // LMB-click-without-drag picks the object under the cursor. No // Blender/Maya preset awareness yet — that arrives with AppSettings. Qt::MouseButton nav_active_button_ = Qt::NoButton; - QPoint nav_last_pos_; - QPoint nav_press_pos_; + Eigen::Vector2i nav_last_pos_; + Eigen::Vector2i nav_press_pos_; bool nav_dragged_ = false; // Benchmark mode. setBenchmarkFrames(N) arms it; render() integrates the