From 4dfe27e251098354c92aa91b4584f499180d3558 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Wed, 27 May 2026 20:28:07 +1000 Subject: [PATCH] wgpu backend: per-element visibility + H/Shift+H/I hotkeys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the interactive selection loop. After this commit you can: - LMB-click an object → highlight (selection) - Press H → hide all selected - Press Shift+H → show all (clear hidden set) - Press I → isolate selected (hide everything else) WgpuVisibilityState (new header) is a plain unordered_set of hidden object_ids — mirrors src/ifcviewer/Visibility.h's shape but stays Qt-free for the ifcviewer-core extract later. cullModelCpuCompute consults visibility_.isHidden(inst.object_id) before the frustum test — hidden instances cost nothing on every axis (no draw, no depth contribution, no pick hit). The CPU vector is read concurrently by the parallel cull workers, which is safe because mutations only happen between renders (handlers requestUpdate after mutating; render reads). Hiding deselects (matches GL behaviour: H clears the now-invisible selection rather than leaving phantom selected-but-invisible ids). Stage 5's last piece — clip planes — is deferred. Adding the uniform array + WGSL discard is mechanical, but the section-tool UI that drives them isn't ported yet (minimal viewer has no way to place a clip plane), so it'd ship as empty plumbing. Will land alongside the section-tool port. Co-Authored-By: Claude Opus 4.7 --- src/ifcviewer-wgpu/WgpuViewportWindow.cpp | 51 ++++++++++++++++++++ src/ifcviewer-wgpu/WgpuViewportWindow.h | 9 ++++ src/ifcviewer-wgpu/WgpuVisibilityState.h | 59 +++++++++++++++++++++++ 3 files changed, 119 insertions(+) create mode 100644 src/ifcviewer-wgpu/WgpuVisibilityState.h diff --git a/src/ifcviewer-wgpu/WgpuViewportWindow.cpp b/src/ifcviewer-wgpu/WgpuViewportWindow.cpp index b820e89034..59d8b7015a 100644 --- a/src/ifcviewer-wgpu/WgpuViewportWindow.cpp +++ b/src/ifcviewer-wgpu/WgpuViewportWindow.cpp @@ -1941,6 +1941,9 @@ uint32_t WgpuViewportWindow::cullModelCpuCompute(WgpuModelGpuData& m, for (uint32_t i = 0; i < uint32_t(m.instances.size()); ++i) { const auto& inst = m.instances[i]; if (inst.mesh_id >= m.meshes.size()) continue; + // Cheapest possible cull first: explicit user-hidden flag. Skips + // every downstream cost (frustum / HiZ / draw / pick). + if (visibility_.isHidden(inst.object_id)) continue; if (!aabbInFrustum(inst.world_aabb_min, inst.world_aabb_max, planes)) continue; const MeshInfo& mesh = m.meshes[inst.mesh_id]; @@ -2972,6 +2975,54 @@ void WgpuViewportWindow::mouseMoveEvent(QMouseEvent* event) { } } +void WgpuViewportWindow::keyPressEvent(QKeyEvent* event) { + // Visibility shortcuts, modelled on the GL viewer: + // H — hide selected + // Shift+H — show all (clear hidden set) + // I — isolate selected (hide everything not currently selected) + // None of these are useful without a selection (except show-all), so we + // skip silently rather than burning a cull on an empty mutation. + const auto mods = event->modifiers(); + const int key = event->key(); + + if (key == Qt::Key_H && (mods & Qt::ShiftModifier)) { + if (visibility_.hiddenCount() == 0) return; + visibility_.clear(); + qInfo() << "[wgpu] show all"; + requestUpdate(); + return; + } + if (key == Qt::Key_H) { + if (selection_.count() == 0) return; + for (uint32_t id : selection_.ids()) visibility_.hide(id); + const size_t n = selection_.count(); + selection_.clear(); // hiding deselects, matching GL behaviour + qInfo().noquote().nospace() << "[wgpu] hid " << n << " selected"; + requestUpdate(); + return; + } + if (key == Qt::Key_I) { + if (selection_.count() == 0) return; + // Walk every instance across all models; hide those NOT in selection. + size_t hidden_now = 0; + for (auto& [mid, m] : models_gpu_) { + for (const auto& inst : m.instances) { + if (selection_.contains(inst.object_id)) continue; + if (!visibility_.isHidden(inst.object_id)) { + visibility_.hide(inst.object_id); + ++hidden_now; + } + } + } + qInfo().noquote().nospace() << "[wgpu] isolated " << selection_.count() + << " (hid " << hidden_now << " others)"; + requestUpdate(); + return; + } + + QWindow::keyPressEvent(event); +} + void WgpuViewportWindow::wheelEvent(QWheelEvent* event) { // 120 = one notch on a typical mouse. Each notch zooms ~12% in/out; // sign matches conventional "wheel up = zoom in". diff --git a/src/ifcviewer-wgpu/WgpuViewportWindow.h b/src/ifcviewer-wgpu/WgpuViewportWindow.h index 5a6d256f57..7a932a4d5c 100644 --- a/src/ifcviewer-wgpu/WgpuViewportWindow.h +++ b/src/ifcviewer-wgpu/WgpuViewportWindow.h @@ -35,6 +35,7 @@ #include "SidecarCache.h" #include "WgpuModelGpuData.h" #include "WgpuSelectionState.h" +#include "WgpuVisibilityState.h" // Stage-2 wgpu viewport: opens a native QWindow, brings up a wgpu instance/ // adapter/device, configures a surface against the platform-native window @@ -106,6 +107,7 @@ protected: void mouseReleaseEvent(QMouseEvent* event) override; void mouseMoveEvent(QMouseEvent* event) override; void wheelEvent(QWheelEvent* event) override; + void keyPressEvent(QKeyEvent* event) override; private: bool initWgpu(); @@ -232,6 +234,13 @@ private: WgpuSelectionState selection_; std::vector selection_flags_scratch_; + // Per-element visibility. Consulted in cullModelCpuCompute to drop + // hidden instances before they're added to visible_draws — keeps + // hidden geometry out of cost on every axis (no draw, no depth, no + // pick). Mutated on the main thread between renders; cull workers + // read concurrently which is safe as long as no concurrent writes. + WgpuVisibilityState visibility_; + // Depth attachment (4× MSAA), recreated on surface resize. WGPUTexture depth_texture_ = nullptr; WGPUTextureView depth_view_ = nullptr; diff --git a/src/ifcviewer-wgpu/WgpuVisibilityState.h b/src/ifcviewer-wgpu/WgpuVisibilityState.h new file mode 100644 index 0000000000..e0895b7a89 --- /dev/null +++ b/src/ifcviewer-wgpu/WgpuVisibilityState.h @@ -0,0 +1,59 @@ +/******************************************************************************** + * * + * 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 WGPUVISIBILITYSTATE_H +#define WGPUVISIBILITYSTATE_H + +#include +#include + +// CPU-side per-element visibility. Mirrors src/ifcviewer/Visibility.h shape +// but pure stdlib so it can move into ifcviewer-core later. +// +// Consulted in cullModelCpuCompute: instances whose object_id is in +// hidden_ids_ are dropped from the visible-draws list entirely (no GPU +// work, no triangle in the depth buffer, no pick hit). Concurrent reads +// from multiple cull worker threads are safe as long as no mutations +// happen during render — which is the case here (input handlers +// requestUpdate after mutating, render then reads). +class WgpuVisibilityState { +public: + bool isHidden(uint32_t object_id) const { + return hidden_ids_.count(object_id) > 0; + } + + void hide(uint32_t object_id) { + if (object_id == 0) return; + hidden_ids_.insert(object_id); + } + + void show(uint32_t object_id) { + hidden_ids_.erase(object_id); + } + + void clear() { hidden_ids_.clear(); } + + size_t hiddenCount() const { return hidden_ids_.size(); } + const std::unordered_set& hiddenIds() const { return hidden_ids_; } + +private: + std::unordered_set hidden_ids_; +}; + +#endif // WGPUVISIBILITYSTATE_H