diff --git a/src/ifcviewer-full/MainWindow.cpp b/src/ifcviewer-full/MainWindow.cpp index d4c4f112af..61b379ce79 100644 --- a/src/ifcviewer-full/MainWindow.cpp +++ b/src/ifcviewer-full/MainWindow.cpp @@ -335,6 +335,20 @@ void MainWindow::setupMenus() { viewport_->toggleVolumeTool(); }, QKeySequence("Ctrl+Shift+V")); view_menu->addSeparator(); + // Element visibility — H hides the current selection, Shift+H + // isolates it (hides everything else within visible models), Alt+H + // restores every element. Model-level hiding is independent and + // stays put. + view_menu->addAction("&Hide Selected", this, [this]() { + viewport_->hideSelectedElements(); + }, QKeySequence(Qt::Key_H)); + view_menu->addAction("&Isolate Selected", this, [this]() { + viewport_->isolateSelectedElements(); + }, QKeySequence(Qt::SHIFT | Qt::Key_H)); + view_menu->addAction("&Show All Elements", this, [this]() { + viewport_->showAllElements(); + }, QKeySequence(Qt::ALT | Qt::Key_H)); + view_menu->addSeparator(); view_menu->addAction("Set &Home View", this, &MainWindow::onSetHomeView); view_menu->addAction("&Go to Home View", this, &MainWindow::onGoHomeView); } diff --git a/src/ifcviewer/ViewportWindow.cpp b/src/ifcviewer/ViewportWindow.cpp index eef4db6938..76f18410cc 100644 --- a/src/ifcviewer/ViewportWindow.cpp +++ b/src/ifcviewer/ViewportWindow.cpp @@ -714,6 +714,14 @@ void ViewportWindow::initGL() { requestUpdate(); emit objectPicked(active_id); }); + // Element-level visibility lives entirely on the CPU (the cull + // consults the flag vector directly), but the cached cull result + // must be invalidated whenever the hidden set changes — otherwise + // a freshly-hidden object would stay drawn on still frames. + connect(&visibility_, &VisibilityState::changed, this, [this]() { + have_cached_cull_ = false; + requestUpdate(); + }); gl_->glEnable(GL_DEPTH_TEST); gl_->glEnable(GL_MULTISAMPLE); @@ -1117,6 +1125,7 @@ void ViewportWindow::uploadInstanceChunk(const InstanceChunk& chunk) { ModelGpuData& m = getOrCreateModel(chunk.model_id); selection_.noteObjectId(chunk.object_id); + visibility_.noteObjectId(chunk.object_id); InstanceCpu inst; inst.mesh_id = chunk.local_mesh_id; @@ -1280,11 +1289,12 @@ void ViewportWindow::applyCachedModel(uint32_t model_id, SidecarData data) { m.instances = std::move(data.instances); // Sidecar path bypasses uploadInstanceChunk, so register every - // instance's object_id with the selection state up front — otherwise - // the per-object_id flags SSBO would be too small for these ids and - // their selection bit reads would silently fall outside the buffer. + // instance's object_id with both the selection and visibility + // states up front — otherwise their per-object_id flag vectors + // would be too small to cover these ids. for (const auto& inst : m.instances) { selection_.noteObjectId(inst.object_id); + visibility_.noteObjectId(inst.object_id); } uint32_t total_tri = 0; @@ -1427,6 +1437,7 @@ void ViewportWindow::resetScene() { } models_gpu_.clear(); selection_.reset(); + visibility_.reset(); have_cached_cull_ = false; requestUpdate(); } @@ -1494,6 +1505,36 @@ void ViewportWindow::setSelectedObjectId(uint32_t id) { selection_.setSelectedObjectId(id); } +void ViewportWindow::hideSelectedElements() { + if (selection_.empty()) return; + visibility_.hideObjects(selection_.selectionIds()); +} + +void ViewportWindow::isolateSelectedElements() { + if (selection_.empty()) return; + // Build the new hidden set: every live object_id in a *visible* + // model that isn't part of the selection. Skipping model-hidden + // objects keeps element-level hidden_ids_ from accumulating ids + // that are already model-hidden — model-hide always wins anyway. + const auto& sel = selection_.selectionIds(); + std::unordered_set new_hidden; + new_hidden.reserve(sel.size() * 8); + for (const auto& [mid, m] : models_gpu_) { + if (m.hidden) continue; + for (const InstanceCpu& inst : m.instances) { + if (inst.object_id == 0) continue; + if (sel.count(inst.object_id) == 0) { + new_hidden.insert(inst.object_id); + } + } + } + visibility_.setHidden(new_hidden); +} + +void ViewportWindow::showAllElements() { + visibility_.showAll(); +} + void ViewportWindow::setCamera(float tx, float ty, float tz, float dist, float yaw, float pitch) { camera_target_ = QVector3D(tx, ty, tz); @@ -2454,6 +2495,11 @@ void ViewportWindow::cullModelCpu(ModelGpuData& m, const float planes[6][4], } // Survivor — now pay the wide-struct fetch for mesh_id. const InstanceCpu& inst = m.instances[inst_idx]; + // Element-level hide check. Done after frustum/contribution/HiZ + // so we don't pay the InstanceCpu fetch on instances that would + // have been culled anyway; for the typical case (a small + // fraction of objects hidden) the wasted cull work is trivial. + if (visibility_.isHidden(inst.object_id)) return; if (inst.mesh_id >= m.meshes.size()) return; const MeshInfo& mesh = m.meshes[inst.mesh_id]; const bool want_lod1 = mesh.lod1_index_count > 0 && diff --git a/src/ifcviewer/ViewportWindow.h b/src/ifcviewer/ViewportWindow.h index 79abb50671..e88e391c74 100644 --- a/src/ifcviewer/ViewportWindow.h +++ b/src/ifcviewer/ViewportWindow.h @@ -49,6 +49,7 @@ QT_END_NAMESPACE #include "OverlayRenderer.h" #include "Selection.h" #include "SidecarCache.h" +#include "Visibility.h" // Matches GL_DRAW_INDIRECT_BUFFER layout for glMultiDrawElementsIndirect. struct DrawElementsIndirectCommand { @@ -328,6 +329,26 @@ public: // or off-surface. std::unordered_set picksInRect(const QRect& rect); + // Per-element visibility (independent of model-level hidden flag). + // External callers go through the convenience verbs below; the + // VisibilityState getter is exposed for read access (e.g. the host + // mirroring its state into a tree's grey-out style). + VisibilityState& visibility() { return visibility_; } + const VisibilityState& visibility() const { return visibility_; } + // Hide every currently-selected element. Selection itself is + // preserved — toggling them back on with showAllElements() leaves + // the same items selected. + void hideSelectedElements(); + // Hide every element NOT in the current selection (limited to + // visible models — model-hidden objects are left as-is so they + // stay model-hidden rather than picking up a redundant + // element-hidden flag too). No-op when nothing is selected. + void isolateSelectedElements(); + // Clear element-level visibility overrides for every loaded model. + // Model-level `hidden` flags are not touched — a hidden model stays + // hidden, matching the user's expectation of "show all *elements*". + void showAllElements(); + // Extended pick: returns the object id, world-space hit point, and // world-space surface normal at (x, y). Renders the same pick pass as // pickObjectAt but reads back from two extra color attachments @@ -729,6 +750,9 @@ private: // Selection — set + active id + per-object_id flags SSBO (binding=3). SelectionState selection_; + // Per-element visibility — CPU-only flag vector consulted by the + // cull, plus a canonical hidden-id set for mutation/reporting. + VisibilityState visibility_; // LMB-press state. press_pick_id_ caches the object hit at press // time so the release path can apply click semantics without a diff --git a/src/ifcviewer/Visibility.cpp b/src/ifcviewer/Visibility.cpp new file mode 100644 index 0000000000..287f768f4b --- /dev/null +++ b/src/ifcviewer/Visibility.cpp @@ -0,0 +1,93 @@ +/******************************************************************************** + * * + * 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 . * + * * + ********************************************************************************/ + +#include "Visibility.h" + +#include + +VisibilityState::VisibilityState(QObject* parent) : QObject(parent) { + // Slot 0 is the "no object" sentinel; keep it present so isHidden(0) + // is well-defined (returns false). + cpu_flags_.assign(1, 0u); +} + +void VisibilityState::noteObjectId(uint32_t id) { + if (id == 0) return; + if (uint32_t(cpu_flags_.size()) <= id) { + cpu_flags_.resize(size_t(id) + 1, 0u); + } +} + +void VisibilityState::reset() { + const bool had_state = !hidden_ids_.empty(); + hidden_ids_.clear(); + std::fill(cpu_flags_.begin(), cpu_flags_.end(), 0u); + if (had_state) emit changed(); +} + +void VisibilityState::hideObjects(const std::unordered_set& ids) { + bool any = false; + for (uint32_t id : ids) { + if (id == 0) continue; + if (hidden_ids_.insert(id).second) { + if (uint32_t(cpu_flags_.size()) <= id) { + cpu_flags_.resize(size_t(id) + 1, 0u); + } + cpu_flags_[id] = 1u; + any = true; + } + } + if (any) emit changed(); +} + +void VisibilityState::showObjects(const std::unordered_set& ids) { + bool any = false; + for (uint32_t id : ids) { + if (hidden_ids_.erase(id) > 0) { + if (id < cpu_flags_.size()) cpu_flags_[id] = 0u; + any = true; + } + } + if (any) emit changed(); +} + +void VisibilityState::setHidden(const std::unordered_set& ids) { + if (ids == hidden_ids_) return; + hidden_ids_ = ids; + hidden_ids_.erase(0); + rebuildFlags(); + emit changed(); +} + +void VisibilityState::showAll() { + if (hidden_ids_.empty()) return; + hidden_ids_.clear(); + std::fill(cpu_flags_.begin(), cpu_flags_.end(), 0u); + emit changed(); +} + +void VisibilityState::rebuildFlags() { + std::fill(cpu_flags_.begin(), cpu_flags_.end(), 0u); + for (uint32_t id : hidden_ids_) { + if (uint32_t(cpu_flags_.size()) <= id) { + cpu_flags_.resize(size_t(id) + 1, 0u); + } + cpu_flags_[id] = 1u; + } +} diff --git a/src/ifcviewer/Visibility.h b/src/ifcviewer/Visibility.h new file mode 100644 index 0000000000..dc51d2e5fc --- /dev/null +++ b/src/ifcviewer/Visibility.h @@ -0,0 +1,89 @@ +/******************************************************************************** + * * + * 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_VISIBILITY_H +#define IFCVIEWER_VISIBILITY_H + +#include + +#include +#include +#include + +// Per-element visibility state, owned by ViewportWindow. Independent of +// the per-model `hidden` flag — model-level hiding still wins (a hidden +// model never draws regardless of its elements' visibility). This class +// only tracks element-level overrides on top. +// +// Lookup is hot — the CPU cull queries `isHidden(object_id)` for every +// surviving instance — so the canonical set is mirrored into a flat +// per-id byte vector indexed directly by object_id. Ownership is purely +// CPU: nothing on the GPU reads visibility, the cull just skips hidden +// instances before they reach the visible[] SSBO. +class VisibilityState : public QObject { + Q_OBJECT +public: + explicit VisibilityState(QObject* parent = nullptr); + + // Tell the manager about a newly added object_id so the flag vector + // can grow ahead of the next cull. Cheap when the id fits already. + void noteObjectId(uint32_t id); + + // Drop everything — clears the hidden set. Called from clearScene. + void reset(); + + // ---- Mutation ---- + // + // hideObjects: union into the hidden set. + // showObjects: subtract from the hidden set. + // setHidden: replace the hidden set wholesale (used by isolate). + // showAll: equivalent to setHidden({}). + void hideObjects(const std::unordered_set& ids); + void showObjects(const std::unordered_set& ids); + void setHidden(const std::unordered_set& ids); + void showAll(); + + // ---- Hot-path query ---- + // + // Inline so the cull's `if (visibility_.isHidden(...)) return;` + // compiles to a bounds check + a byte load + a compare. + bool isHidden(uint32_t id) const { + return id < cpu_flags_.size() && cpu_flags_[id] != 0; + } + + // ---- Accessors ---- + bool empty() const { return hidden_ids_.empty(); } + size_t size() const { return hidden_ids_.size(); } + const std::unordered_set& hiddenIds() const { return hidden_ids_; } + +signals: + // Emitted on any mutation that changes the hidden set. Consumers + // (the viewport) connect to invalidate cached cull state and + // requestUpdate. + void changed(); + +private: + // Recompute cpu_flags_ from hidden_ids_. Cheap: O(|cpu_flags_|). + void rebuildFlags(); + + std::unordered_set hidden_ids_; + std::vector cpu_flags_; +}; + +#endif // IFCVIEWER_VISIBILITY_H