ifcviewer: per-element visibility (H / Shift+H / Alt+H)

Adds VisibilityState, a CPU-only sibling to SelectionState.  It owns
the canonical hidden-id set plus a flat per-object_id byte vector that
the cull's hot path queries inline (bounds check + byte load + compare
per surviving instance).  Hidden elements never reach the visible[]
SSBO so they don't draw or pick — matching Blender/CAD convention.

ViewportWindow registers every streamed and sidecar-cached object_id
with the new state, resets it on clearScene, and connects the changed
signal to invalidate cached cull state.  Three convenience verbs:
hideSelectedElements (union into hidden), isolateSelectedElements
(replace hidden with live-object_ids minus selection, skipping
model-hidden models so element-hide doesn't pile on top of model-hide),
and showAllElements (clears the override; model-hidden models stay
hidden, per the user's spec).

Bound in the View menu: H hide, Shift+H isolate, Alt+H show all.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Dion Moult
2026-05-11 07:33:09 +10:00
parent 742561c56d
commit 1c598a981e
5 changed files with 269 additions and 3 deletions
+49 -3
View File
@@ -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<uint32_t> 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 &&