mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-13 02:47:48 +00:00
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:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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<uint32_t> 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
|
||||
|
||||
@@ -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 <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
#include "Visibility.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
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<uint32_t>& 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<uint32_t>& 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<uint32_t>& 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;
|
||||
}
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
#ifndef IFCVIEWER_VISIBILITY_H
|
||||
#define IFCVIEWER_VISIBILITY_H
|
||||
|
||||
#include <QObject>
|
||||
|
||||
#include <cstdint>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
// 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<uint32_t>& ids);
|
||||
void showObjects(const std::unordered_set<uint32_t>& ids);
|
||||
void setHidden(const std::unordered_set<uint32_t>& 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<uint32_t>& 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<uint32_t> hidden_ids_;
|
||||
std::vector<uint8_t> cpu_flags_;
|
||||
};
|
||||
|
||||
#endif // IFCVIEWER_VISIBILITY_H
|
||||
Reference in New Issue
Block a user