From b73cdd1a0e15a9e163dc5a34ef5838bca744b505 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Wed, 29 Apr 2026 08:54:35 +1000 Subject: [PATCH] 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 --- src/ifcviewer-full/SettingsWindow.cpp | 11 ++ src/ifcviewer-full/SettingsWindow.h | 2 + src/ifcviewer/AppSettings.cpp | 17 ++ src/ifcviewer/AppSettings.h | 8 + src/ifcviewer/GeometryStreamer.cpp | 262 +++++++++++++++++--------- 5 files changed, 211 insertions(+), 89 deletions(-) diff --git a/src/ifcviewer-full/SettingsWindow.cpp b/src/ifcviewer-full/SettingsWindow.cpp index 1f31ceacf7..b5b22279fd 100644 --- a/src/ifcviewer-full/SettingsWindow.cpp +++ b/src/ifcviewer-full/SettingsWindow.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include SettingsWindow::SettingsWindow(QWidget *parent) @@ -57,6 +58,14 @@ void SettingsWindow::setupUi() { "and, on sidecar hits, avoids a second file read."); 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( QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); @@ -80,6 +89,7 @@ void SettingsWindow::syncFromSettings() { show_stats_check_->setChecked(AppSettings::instance().showStats()); backface_culling_check_->setChecked(AppSettings::instance().backfaceCulling()); load_data_source_check_->setChecked(AppSettings::instance().loadDataSource()); + void_limit_spin_->setValue(AppSettings::instance().voidLimit()); } void SettingsWindow::onAccepted() { @@ -87,5 +97,6 @@ void SettingsWindow::onAccepted() { AppSettings::instance().setShowStats(show_stats_check_->isChecked()); AppSettings::instance().setBackfaceCulling(backface_culling_check_->isChecked()); AppSettings::instance().setLoadDataSource(load_data_source_check_->isChecked()); + AppSettings::instance().setVoidLimit(void_limit_spin_->value()); accept(); } diff --git a/src/ifcviewer-full/SettingsWindow.h b/src/ifcviewer-full/SettingsWindow.h index d7399c1e7c..70c4442fc2 100644 --- a/src/ifcviewer-full/SettingsWindow.h +++ b/src/ifcviewer-full/SettingsWindow.h @@ -25,6 +25,7 @@ class QCheckBox; class QLineEdit; class QShowEvent; +class QSpinBox; class SettingsWindow : public QDialog { Q_OBJECT @@ -45,6 +46,7 @@ private: QCheckBox* show_stats_check_ = nullptr; QCheckBox* backface_culling_check_ = nullptr; QCheckBox* load_data_source_check_ = nullptr; + QSpinBox* void_limit_spin_ = nullptr; }; #endif diff --git a/src/ifcviewer/AppSettings.cpp b/src/ifcviewer/AppSettings.cpp index 04f56f9567..58dbfe33be 100644 --- a/src/ifcviewer/AppSettings.cpp +++ b/src/ifcviewer/AppSettings.cpp @@ -27,6 +27,8 @@ constexpr const char* kGeometryLibraryDefault = "hybrid-cgal-simple-opencascade" constexpr const char* kShowStatsKey = "viewport/show_stats"; constexpr const char* kBackfaceCullingKey = "viewport/backface_culling"; constexpr const char* kLoadDataSourceKey = "loading/load_data_source"; +constexpr const char* kVoidLimitKey = "loading/void_limit"; +constexpr int kVoidLimitDefault = 30; } AppSettings& AppSettings::instance() { @@ -82,12 +84,26 @@ void AppSettings::setLoadDataSource(bool 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() { QSettings settings; geometry_library_ = settings.value(kGeometryLibraryKey, kGeometryLibraryDefault).toString(); show_stats_ = settings.value(kShowStatsKey, false).toBool(); backface_culling_ = settings.value(kBackfaceCullingKey, 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() { @@ -96,4 +112,5 @@ void AppSettings::persist() { settings.setValue(kShowStatsKey, show_stats_); settings.setValue(kBackfaceCullingKey, backface_culling_); settings.setValue(kLoadDataSourceKey, load_data_source_); + settings.setValue(kVoidLimitKey, void_limit_); } diff --git a/src/ifcviewer/AppSettings.h b/src/ifcviewer/AppSettings.h index 9b909bc48c..5e796280aa 100644 --- a/src/ifcviewer/AppSettings.h +++ b/src/ifcviewer/AppSettings.h @@ -47,11 +47,18 @@ public: bool loadDataSource() const; 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: void geometryLibraryChanged(const QString& value); void showStatsChanged(bool value); void backfaceCullingChanged(bool value); void loadDataSourceChanged(bool value); + void voidLimitChanged(int value); private: AppSettings(); @@ -62,6 +69,7 @@ private: bool show_stats_ = false; bool backface_culling_ = true; bool load_data_source_ = true; + int void_limit_ = 30; }; #endif // APPSETTINGS_H diff --git a/src/ifcviewer/GeometryStreamer.cpp b/src/ifcviewer/GeometryStreamer.cpp index 668bb1c7ce..59a5618aa7 100644 --- a/src/ifcviewer/GeometryStreamer.cpp +++ b/src/ifcviewer/GeometryStreamer.cpp @@ -21,6 +21,8 @@ #include "AppSettings.h" #include "../ifcgeom/hybrid_kernel.h" #include "../ifcgeom/taxonomy.h" +#include "../ifcgeom/IfcGeomFilter.h" +#include "../ifcparse/express.h" #include @@ -30,6 +32,7 @@ #include #include #include +#include #include #include @@ -294,30 +297,59 @@ void GeometryStreamer::run(const std::string& path, int num_threads) { const bool is_rocksdb = std::holds_alternative(ifc_file_->storage_); const int effective_threads = is_rocksdb ? 1 : num_threads; - std::unique_ptr iterator; - try { - const std::string geometry_library = - AppSettings::instance().geometryLibrary().toStdString(); - auto kernel = ifcopenshell::geometry::kernels::construct( - ifc_file_.get(), geometry_library, settings); - iterator = std::make_unique( - std::move(kernel), settings, ifc_file_.get(), - std::vector(), effective_threads); - } catch (const std::exception& e) { - emit errorOccurred(QString("Failed to create geometry iterator: %1").arg(e.what())); - return; + // 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 net_ids; + std::set gross_ids; + { + const std::string& schema_name = ifc_file_->schema()->name(); + std::vector 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 { + opening_count = static_cast( + e.as().get_inverse("HasOpenings").size()); + } catch (...) { + // HasOpenings not declared on this entity — treat as 0. + } + } + if (opening_count > void_limit) { + gross_ids.insert(e.id()); + } else { + net_ids.insert(e.id()); + } + } } - if (!iterator->initialize()) { - emit errorOccurred("No geometry found in IFC file"); + if (net_ids.empty() && gross_ids.empty()) { + emit errorOccurred("No geometry-bearing elements found in IFC file"); return; } + if (!gross_ids.empty()) { + qDebug("Excessive voids: %zu element(s) will be loaded without " + "opening subtractions", + gross_ids.size()); + } - int last_progress = 0; - - // geom.id() → local_mesh_id within this model. + // Shared dedup + AABB state across passes — same geom.id() across + // net/gross passes still maps to one mesh upload. std::unordered_map 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]; }; std::vector mesh_aabbs; @@ -326,92 +358,144 @@ void GeometryStreamer::run(const std::string& path, int num_threads) { QElapsedTimer stream_timer; stream_timer.start(); - do { - if (cancel_requested_.load()) break; + // 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(100.0 * net_ids.size() / total_count + 0.5); - const IfcGeom::Element* elem = iterator->get(); - if (!elem) continue; + auto run_pass = [&](const std::set& include_ids, + bool is_gross, + int progress_lo, + int progress_hi) -> bool { + if (include_ids.empty()) return true; - const auto* tri_elem = dynamic_cast(elem); - if (!tri_elem) continue; - - const auto& geom = tri_elem->geometry(); - if (geom.verts().empty() || geom.faces().empty()) continue; - - uint32_t object_id = next_object_id_++; - - // Element metadata. - ElementInfo info; - info.object_id = object_id; - info.model_id = model_id_; - info.ifc_id = tri_elem->id(); - info.guid = tri_elem->guid(); - info.name = tri_elem->name(); - info.type = tri_elem->type(); - info.parent_id = tri_elem->parent_id(); - { - std::lock_guard lock(elements_mutex_); - pending_elements_.push_back(std::move(info)); + ifcopenshell::geometry::Settings pass_settings = settings; + if (is_gross) { + pass_settings.set("disable-opening-subtractions", true); } - // Representation dedup. - const std::string& geom_id = geom.id(); - uint32_t local_mesh_id; - bool first_sight = false; - if (geom_id.empty()) { - // No representation key — treat as unique. - local_mesh_id = total_meshes++; - first_sight = true; - } else { - auto it = geom_to_local_mesh_id.find(geom_id); - if (it == geom_to_local_mesh_id.end()) { + std::vector filters; + IfcGeom::instance_id_filter idf{ + /*include=*/true, /*traverse=*/false, include_ids}; + filters.push_back(idf); + + std::unique_ptr 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( + 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 { + if (cancel_requested_.load()) break; + + const IfcGeom::Element* elem = iterator->get(); + if (!elem) continue; + + const auto* tri_elem = dynamic_cast(elem); + if (!tri_elem) continue; + + const auto& geom = tri_elem->geometry(); + if (geom.verts().empty() || geom.faces().empty()) continue; + + uint32_t object_id = next_object_id_++; + + ElementInfo info; + info.object_id = object_id; + info.model_id = model_id_; + info.ifc_id = tri_elem->id(); + info.guid = tri_elem->guid(); + info.name = tri_elem->name(); + info.type = tri_elem->type(); + info.parent_id = tri_elem->parent_id(); + { + std::lock_guard lock(elements_mutex_); + pending_elements_.push_back(std::move(info)); + } + + const std::string& geom_id = geom.id(); + uint32_t local_mesh_id; + bool first_sight = false; + if (geom_id.empty()) { local_mesh_id = total_meshes++; - geom_to_local_mesh_id.emplace(geom_id, local_mesh_id); first_sight = true; } else { - local_mesh_id = it->second; + auto it = geom_to_local_mesh_id.find(geom_id); + if (it == geom_to_local_mesh_id.end()) { + local_mesh_id = total_meshes++; + geom_to_local_mesh_id.emplace(geom_id, local_mesh_id); + first_sight = true; + } else { + local_mesh_id = it->second; + } } - } - if (first_sight) { - MeshChunk mesh_chunk = buildMeshChunk(model_id_, local_mesh_id, tri_elem); - MeshAabb ma; - for (int a = 0; a < 3; ++a) { - ma.lmin[a] = mesh_chunk.local_aabb_min[a]; - ma.lmax[a] = mesh_chunk.local_aabb_max[a]; + if (first_sight) { + MeshChunk mesh_chunk = buildMeshChunk(model_id_, local_mesh_id, tri_elem); + MeshAabb ma; + for (int a = 0; a < 3; ++a) { + ma.lmin[a] = mesh_chunk.local_aabb_min[a]; + ma.lmax[a] = mesh_chunk.local_aabb_max[a]; + } + if (mesh_aabbs.size() <= local_mesh_id) mesh_aabbs.resize(local_mesh_id + 1); + mesh_aabbs[local_mesh_id] = ma; + if (!mesh_chunk.indices.empty()) { + emit meshReady(std::move(mesh_chunk)); + } } - if (mesh_aabbs.size() <= local_mesh_id) mesh_aabbs.resize(local_mesh_id + 1); - mesh_aabbs[local_mesh_id] = ma; - if (!mesh_chunk.indices.empty()) { - emit meshReady(std::move(mesh_chunk)); + + const Eigen::Matrix4d& mat_d = tri_elem->transformation().data()->ccomponents(); + InstanceChunk inst; + inst.model_id = model_id_; + inst.local_mesh_id = local_mesh_id; + inst.object_id = object_id; + inst.color_override_rgba8 = 0; + for (int i = 0; i < 16; ++i) { + inst.transform[i] = static_cast(mat_d.data()[i]); } - } - // Transform (column-major 4x4, cast to float). - const Eigen::Matrix4d& mat_d = tri_elem->transformation().data()->ccomponents(); - InstanceChunk inst; - inst.model_id = model_id_; - inst.local_mesh_id = local_mesh_id; - inst.object_id = object_id; - inst.color_override_rgba8 = 0; // 0 = use baked vertex color - for (int i = 0; i < 16; ++i) { - inst.transform[i] = static_cast(mat_d.data()[i]); - } + const MeshAabb& ma = mesh_aabbs[local_mesh_id]; + worldAabbFromLocal(ma.lmin, ma.lmax, inst.transform, + inst.world_aabb_min, inst.world_aabb_max); - const MeshAabb& ma = mesh_aabbs[local_mesh_id]; - worldAabbFromLocal(ma.lmin, ma.lmax, inst.transform, - inst.world_aabb_min, inst.world_aabb_max); + emit instanceReady(std::move(inst)); + total_shapes++; - emit instanceReady(std::move(inst)); - total_shapes++; + const int p = progress_lo + + (iterator->progress() * (progress_hi - progress_lo)) / 100; + if (p != last_progress) { + last_progress = p; + progress_ = p; + emit progressChanged(p); + } + } while (iterator->next()); - int p = iterator->progress(); - if (p != last_progress) { - last_progress = p; - progress_ = p; - emit progressChanged(p); - } - } 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; emit progressChanged(100);