ifcviewer: filter iterator to net IfcElements, void-limit setting

Mirror bonsai's IfcImporter.process_element_filter so the streamer
walks only IfcElement (plus IfcProxy on IFC2X3/IFC4), drops
IfcFeatureElement except IfcSurfaceFeature, and routes elements
with more openings than the configurable void limit through a
second iterator pass with disable-opening-subtractions=true.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Dion Moult
2026-04-29 08:54:35 +10:00
parent e21bd1ac96
commit b73cdd1a0e
5 changed files with 211 additions and 89 deletions
+11
View File
@@ -25,6 +25,7 @@
#include <QFormLayout> #include <QFormLayout>
#include <QLineEdit> #include <QLineEdit>
#include <QShowEvent> #include <QShowEvent>
#include <QSpinBox>
#include <QVBoxLayout> #include <QVBoxLayout>
SettingsWindow::SettingsWindow(QWidget *parent) SettingsWindow::SettingsWindow(QWidget *parent)
@@ -57,6 +58,14 @@ void SettingsWindow::setupUi() {
"and, on sidecar hits, avoids a second file read."); "and, on sidecar hits, avoids a second file read.");
form->addRow("Load Property Data Source", load_data_source_check_); form->addRow("Load Property Data Source", load_data_source_check_);
void_limit_spin_ = new QSpinBox(this);
void_limit_spin_->setRange(0, 100000);
void_limit_spin_->setToolTip(
"Skip elements with more openings (HasOpenings) than this. "
"A handful of pathological elements can dominate boolean-subtraction "
"time; dropping them keeps load times sane.");
form->addRow("Void Limit", void_limit_spin_);
auto* button_box = new QDialogButtonBox( auto* button_box = new QDialogButtonBox(
QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
@@ -80,6 +89,7 @@ void SettingsWindow::syncFromSettings() {
show_stats_check_->setChecked(AppSettings::instance().showStats()); show_stats_check_->setChecked(AppSettings::instance().showStats());
backface_culling_check_->setChecked(AppSettings::instance().backfaceCulling()); backface_culling_check_->setChecked(AppSettings::instance().backfaceCulling());
load_data_source_check_->setChecked(AppSettings::instance().loadDataSource()); load_data_source_check_->setChecked(AppSettings::instance().loadDataSource());
void_limit_spin_->setValue(AppSettings::instance().voidLimit());
} }
void SettingsWindow::onAccepted() { void SettingsWindow::onAccepted() {
@@ -87,5 +97,6 @@ void SettingsWindow::onAccepted() {
AppSettings::instance().setShowStats(show_stats_check_->isChecked()); AppSettings::instance().setShowStats(show_stats_check_->isChecked());
AppSettings::instance().setBackfaceCulling(backface_culling_check_->isChecked()); AppSettings::instance().setBackfaceCulling(backface_culling_check_->isChecked());
AppSettings::instance().setLoadDataSource(load_data_source_check_->isChecked()); AppSettings::instance().setLoadDataSource(load_data_source_check_->isChecked());
AppSettings::instance().setVoidLimit(void_limit_spin_->value());
accept(); accept();
} }
+2
View File
@@ -25,6 +25,7 @@
class QCheckBox; class QCheckBox;
class QLineEdit; class QLineEdit;
class QShowEvent; class QShowEvent;
class QSpinBox;
class SettingsWindow : public QDialog { class SettingsWindow : public QDialog {
Q_OBJECT Q_OBJECT
@@ -45,6 +46,7 @@ private:
QCheckBox* show_stats_check_ = nullptr; QCheckBox* show_stats_check_ = nullptr;
QCheckBox* backface_culling_check_ = nullptr; QCheckBox* backface_culling_check_ = nullptr;
QCheckBox* load_data_source_check_ = nullptr; QCheckBox* load_data_source_check_ = nullptr;
QSpinBox* void_limit_spin_ = nullptr;
}; };
#endif #endif
+17
View File
@@ -27,6 +27,8 @@ constexpr const char* kGeometryLibraryDefault = "hybrid-cgal-simple-opencascade"
constexpr const char* kShowStatsKey = "viewport/show_stats"; constexpr const char* kShowStatsKey = "viewport/show_stats";
constexpr const char* kBackfaceCullingKey = "viewport/backface_culling"; constexpr const char* kBackfaceCullingKey = "viewport/backface_culling";
constexpr const char* kLoadDataSourceKey = "loading/load_data_source"; constexpr const char* kLoadDataSourceKey = "loading/load_data_source";
constexpr const char* kVoidLimitKey = "loading/void_limit";
constexpr int kVoidLimitDefault = 30;
} }
AppSettings& AppSettings::instance() { AppSettings& AppSettings::instance() {
@@ -82,12 +84,26 @@ void AppSettings::setLoadDataSource(bool value) {
emit loadDataSourceChanged(value); emit loadDataSourceChanged(value);
} }
int AppSettings::voidLimit() const {
return void_limit_;
}
void AppSettings::setVoidLimit(int value) {
if (value < 0) value = 0;
if (void_limit_ == value) return;
void_limit_ = value;
persist();
emit voidLimitChanged(value);
}
void AppSettings::load() { void AppSettings::load() {
QSettings settings; QSettings settings;
geometry_library_ = settings.value(kGeometryLibraryKey, kGeometryLibraryDefault).toString(); geometry_library_ = settings.value(kGeometryLibraryKey, kGeometryLibraryDefault).toString();
show_stats_ = settings.value(kShowStatsKey, false).toBool(); show_stats_ = settings.value(kShowStatsKey, false).toBool();
backface_culling_ = settings.value(kBackfaceCullingKey, true).toBool(); backface_culling_ = settings.value(kBackfaceCullingKey, true).toBool();
load_data_source_ = settings.value(kLoadDataSourceKey, true).toBool(); load_data_source_ = settings.value(kLoadDataSourceKey, true).toBool();
void_limit_ = settings.value(kVoidLimitKey, kVoidLimitDefault).toInt();
if (void_limit_ < 0) void_limit_ = 0;
} }
void AppSettings::persist() { void AppSettings::persist() {
@@ -96,4 +112,5 @@ void AppSettings::persist() {
settings.setValue(kShowStatsKey, show_stats_); settings.setValue(kShowStatsKey, show_stats_);
settings.setValue(kBackfaceCullingKey, backface_culling_); settings.setValue(kBackfaceCullingKey, backface_culling_);
settings.setValue(kLoadDataSourceKey, load_data_source_); settings.setValue(kLoadDataSourceKey, load_data_source_);
settings.setValue(kVoidLimitKey, void_limit_);
} }
+8
View File
@@ -47,11 +47,18 @@ public:
bool loadDataSource() const; bool loadDataSource() const;
void setLoadDataSource(bool value); void setLoadDataSource(bool value);
// Skip elements with more than this many voids (HasOpenings inverse).
// Boolean subtraction of many openings is the dominant cost in some
// pathological exports; dropping those elements keeps load times sane.
int voidLimit() const;
void setVoidLimit(int value);
signals: signals:
void geometryLibraryChanged(const QString& value); void geometryLibraryChanged(const QString& value);
void showStatsChanged(bool value); void showStatsChanged(bool value);
void backfaceCullingChanged(bool value); void backfaceCullingChanged(bool value);
void loadDataSourceChanged(bool value); void loadDataSourceChanged(bool value);
void voidLimitChanged(int value);
private: private:
AppSettings(); AppSettings();
@@ -62,6 +69,7 @@ private:
bool show_stats_ = false; bool show_stats_ = false;
bool backface_culling_ = true; bool backface_culling_ = true;
bool load_data_source_ = true; bool load_data_source_ = true;
int void_limit_ = 30;
}; };
#endif // APPSETTINGS_H #endif // APPSETTINGS_H
+107 -23
View File
@@ -21,6 +21,8 @@
#include "AppSettings.h" #include "AppSettings.h"
#include "../ifcgeom/hybrid_kernel.h" #include "../ifcgeom/hybrid_kernel.h"
#include "../ifcgeom/taxonomy.h" #include "../ifcgeom/taxonomy.h"
#include "../ifcgeom/IfcGeomFilter.h"
#include "../ifcparse/express.h"
#include <Eigen/Dense> #include <Eigen/Dense>
@@ -30,6 +32,7 @@
#include <cstring> #include <cstring>
#include <algorithm> #include <algorithm>
#include <limits> #include <limits>
#include <set>
#include <QDebug> #include <QDebug>
#include <QElapsedTimer> #include <QElapsedTimer>
@@ -294,30 +297,59 @@ void GeometryStreamer::run(const std::string& path, int num_threads) {
const bool is_rocksdb = std::holds_alternative<ifcopenshell::impl::rocks_db_file_storage>(ifc_file_->storage_); const bool is_rocksdb = std::holds_alternative<ifcopenshell::impl::rocks_db_file_storage>(ifc_file_->storage_);
const int effective_threads = is_rocksdb ? 1 : num_threads; const int effective_threads = is_rocksdb ? 1 : num_threads;
std::unique_ptr<IfcGeom::Iterator> iterator; // Mirror bonsai's IfcImporter.process_element_filter: walk IfcElement
// (plus IfcProxy on IFC2X3/IFC4), drop IfcFeatureElement except
// IfcSurfaceFeature, and split elements with more openings than the
// configured void limit into a "gross" set that is rendered without
// opening subtractions. Both sets become include filters so we don't
// waste time mapping openings, spaces, grids, etc.
std::set<int> net_ids;
std::set<int> gross_ids;
{
const std::string& schema_name = ifc_file_->schema()->name();
std::vector<express::Base> elements =
ifc_file_->instances_by_type("IfcElement");
if (schema_name == "IFC2X3" || schema_name == "IFC4") {
auto proxies = ifc_file_->instances_by_type("IfcProxy");
elements.insert(elements.end(), proxies.begin(), proxies.end());
}
const int void_limit = AppSettings::instance().voidLimit();
for (const auto& e : elements) {
const auto& decl = e.declaration();
if (decl.is("IfcFeatureElement") && !decl.is("IfcSurfaceFeature")) {
continue;
}
int opening_count = 0;
if (decl.is("IfcElement")) {
try { try {
const std::string geometry_library = opening_count = static_cast<int>(
AppSettings::instance().geometryLibrary().toStdString(); e.as<express::Entity>().get_inverse("HasOpenings").size());
auto kernel = ifcopenshell::geometry::kernels::construct( } catch (...) {
ifc_file_.get(), geometry_library, settings); // HasOpenings not declared on this entity — treat as 0.
iterator = std::make_unique<IfcGeom::Iterator>( }
std::move(kernel), settings, ifc_file_.get(), }
std::vector<ifcopenshell::geometry::filter_t>(), effective_threads); if (opening_count > void_limit) {
} catch (const std::exception& e) { gross_ids.insert(e.id());
emit errorOccurred(QString("Failed to create geometry iterator: %1").arg(e.what())); } else {
return; net_ids.insert(e.id());
}
}
} }
if (!iterator->initialize()) { if (net_ids.empty() && gross_ids.empty()) {
emit errorOccurred("No geometry found in IFC file"); emit errorOccurred("No geometry-bearing elements found in IFC file");
return; return;
} }
if (!gross_ids.empty()) {
qDebug("Excessive voids: %zu element(s) will be loaded without "
"opening subtractions",
gross_ids.size());
}
int last_progress = 0; // Shared dedup + AABB state across passes — same geom.id() across
// net/gross passes still maps to one mesh upload.
// geom.id() → local_mesh_id within this model.
std::unordered_map<std::string, uint32_t> geom_to_local_mesh_id; std::unordered_map<std::string, uint32_t> geom_to_local_mesh_id;
// local_mesh_id → (local AABB) so we can derive world AABBs for later instances.
struct MeshAabb { float lmin[3], lmax[3]; }; struct MeshAabb { float lmin[3], lmax[3]; };
std::vector<MeshAabb> mesh_aabbs; std::vector<MeshAabb> mesh_aabbs;
@@ -326,6 +358,53 @@ void GeometryStreamer::run(const std::string& path, int num_threads) {
QElapsedTimer stream_timer; QElapsedTimer stream_timer;
stream_timer.start(); stream_timer.start();
// Split the 0–100 progress range proportionally to element counts so
// the bar advances roughly with wall time across both passes.
const size_t total_count = net_ids.size() + gross_ids.size();
const int net_progress_end = total_count == 0
? 100
: static_cast<int>(100.0 * net_ids.size() / total_count + 0.5);
auto run_pass = [&](const std::set<int>& include_ids,
bool is_gross,
int progress_lo,
int progress_hi) -> bool {
if (include_ids.empty()) return true;
ifcopenshell::geometry::Settings pass_settings = settings;
if (is_gross) {
pass_settings.set("disable-opening-subtractions", true);
}
std::vector<ifcopenshell::geometry::filter_t> filters;
IfcGeom::instance_id_filter idf{
/*include=*/true, /*traverse=*/false, include_ids};
filters.push_back(idf);
std::unique_ptr<IfcGeom::Iterator> iterator;
try {
const std::string geometry_library =
AppSettings::instance().geometryLibrary().toStdString();
auto kernel = ifcopenshell::geometry::kernels::construct(
ifc_file_.get(), geometry_library, pass_settings);
iterator = std::make_unique<IfcGeom::Iterator>(
std::move(kernel), pass_settings, ifc_file_.get(),
filters, effective_threads);
} catch (const std::exception& e) {
emit errorOccurred(QString("Failed to create geometry iterator: %1").arg(e.what()));
return false;
}
if (!iterator->initialize()) {
// Empty pass — no geometry survived for these ids. Still
// advance progress to the upper bound so the bar doesn't stall.
progress_ = progress_hi;
emit progressChanged(progress_hi);
return true;
}
int last_progress = progress_lo;
do { do {
if (cancel_requested_.load()) break; if (cancel_requested_.load()) break;
@@ -340,7 +419,6 @@ void GeometryStreamer::run(const std::string& path, int num_threads) {
uint32_t object_id = next_object_id_++; uint32_t object_id = next_object_id_++;
// Element metadata.
ElementInfo info; ElementInfo info;
info.object_id = object_id; info.object_id = object_id;
info.model_id = model_id_; info.model_id = model_id_;
@@ -354,12 +432,10 @@ void GeometryStreamer::run(const std::string& path, int num_threads) {
pending_elements_.push_back(std::move(info)); pending_elements_.push_back(std::move(info));
} }
// Representation dedup.
const std::string& geom_id = geom.id(); const std::string& geom_id = geom.id();
uint32_t local_mesh_id; uint32_t local_mesh_id;
bool first_sight = false; bool first_sight = false;
if (geom_id.empty()) { if (geom_id.empty()) {
// No representation key — treat as unique.
local_mesh_id = total_meshes++; local_mesh_id = total_meshes++;
first_sight = true; first_sight = true;
} else { } else {
@@ -387,13 +463,12 @@ void GeometryStreamer::run(const std::string& path, int num_threads) {
} }
} }
// Transform (column-major 4x4, cast to float).
const Eigen::Matrix4d& mat_d = tri_elem->transformation().data()->ccomponents(); const Eigen::Matrix4d& mat_d = tri_elem->transformation().data()->ccomponents();
InstanceChunk inst; InstanceChunk inst;
inst.model_id = model_id_; inst.model_id = model_id_;
inst.local_mesh_id = local_mesh_id; inst.local_mesh_id = local_mesh_id;
inst.object_id = object_id; inst.object_id = object_id;
inst.color_override_rgba8 = 0; // 0 = use baked vertex color inst.color_override_rgba8 = 0;
for (int i = 0; i < 16; ++i) { for (int i = 0; i < 16; ++i) {
inst.transform[i] = static_cast<float>(mat_d.data()[i]); inst.transform[i] = static_cast<float>(mat_d.data()[i]);
} }
@@ -405,7 +480,8 @@ void GeometryStreamer::run(const std::string& path, int num_threads) {
emit instanceReady(std::move(inst)); emit instanceReady(std::move(inst));
total_shapes++; total_shapes++;
int p = iterator->progress(); const int p = progress_lo +
(iterator->progress() * (progress_hi - progress_lo)) / 100;
if (p != last_progress) { if (p != last_progress) {
last_progress = p; last_progress = p;
progress_ = p; progress_ = p;
@@ -413,6 +489,14 @@ void GeometryStreamer::run(const std::string& path, int num_threads) {
} }
} while (iterator->next()); } while (iterator->next());
return true;
};
if (!run_pass(net_ids, /*is_gross=*/false, 0, net_progress_end)) return;
if (!cancel_requested_.load()) {
run_pass(gross_ids, /*is_gross=*/true, net_progress_end, 100);
}
progress_ = 100; progress_ = 100;
emit progressChanged(100); emit progressChanged(100);