From 4520a721529240e41065031e030bafea98916ab6 Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Wed, 27 May 2026 14:55:25 +0200 Subject: [PATCH 01/12] Sane error messages for unsupported items in geometry libs #8106 --- src/ifcgeom/AbstractKernel.h | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/ifcgeom/AbstractKernel.h b/src/ifcgeom/AbstractKernel.h index cd29246710..3e2be1b1f0 100644 --- a/src/ifcgeom/AbstractKernel.h +++ b/src/ifcgeom/AbstractKernel.h @@ -149,8 +149,12 @@ namespace { template <> struct dispatch_conversion { - static bool dispatch(ifcopenshell::geometry::kernels::AbstractKernel*, ifcopenshell::geometry::taxonomy::kinds, const ifcopenshell::geometry::taxonomy::ptr& item, IfcGeom::ConversionResults&) { - Logger::Error("No conversion for " + std::to_string(item->kind())); + static bool dispatch(ifcopenshell::geometry::kernels::AbstractKernel* kernel, ifcopenshell::geometry::taxonomy::kinds, const ifcopenshell::geometry::taxonomy::ptr& item, IfcGeom::ConversionResults&) { + std::string created_from; + if (item->instance) { + created_from = " (created from " + item->instance->declaration().name() + ")"; + } + Logger::Error("No support for " + ifcopenshell::geometry::taxonomy::kind_to_string(item->kind()) + created_from + " in kernel " + kernel->geometry_library()); return false; } }; @@ -169,8 +173,12 @@ namespace { template <> struct dispatch_with_upgrade { - static bool dispatch(ifcopenshell::geometry::kernels::AbstractKernel*, const ifcopenshell::geometry::taxonomy::ptr& item, IfcGeom::ConversionResults&) { - Logger::Error("No conversion with upgrade for " + std::to_string(item->kind())); + static bool dispatch(ifcopenshell::geometry::kernels::AbstractKernel* kernel, const ifcopenshell::geometry::taxonomy::ptr& item, IfcGeom::ConversionResults&) { + std::string created_from; + if (item->instance) { + created_from = " (created from " + item->instance->declaration().name() + ")"; + } + Logger::Error("No support (after considering item upgrade) for " + ifcopenshell::geometry::taxonomy::kind_to_string(item->kind()) + created_from + " in kernel " + kernel->geometry_library()); return false; } }; From 77a2284f8a8a6c0542699d552ac970215c0577ba Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Thu, 4 Jun 2026 21:37:41 +0200 Subject: [PATCH 02/12] Re-sew non-manifold operands; interior loop re-orientations affect edge identity #8140 --- src/ifcgeom/kernels/opencascade/OpenCascadeKernel.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/ifcgeom/kernels/opencascade/OpenCascadeKernel.cpp b/src/ifcgeom/kernels/opencascade/OpenCascadeKernel.cpp index e8b672906e..5284a26a5b 100644 --- a/src/ifcgeom/kernels/opencascade/OpenCascadeKernel.cpp +++ b/src/ifcgeom/kernels/opencascade/OpenCascadeKernel.cpp @@ -136,6 +136,17 @@ bool IfcGeom::OpenCascadeKernel::convert_openings(const IfcUtil::IfcBaseEntity* for (auto entity_part : parts) { bool is_manifold = util::is_manifold(entity_part); + if (!is_manifold) { + // force sewing, edge identity might have been mudied by FixAdvFace.FixOrientation.MSG5 to fix interior loop winding order + TopTools_ListOfShape list; + IfcGeom::util::shape_to_face_list(entity_part, list); + IfcGeom::util::create_solid_from_faces(list, entity_part, settings_.get().get(), true); + is_manifold = util::is_manifold(entity_part); + if (is_manifold) { + Logger::Warning("Successfully sewed non-manifold first operand"); + } + } + if (!is_manifold) { if (settings_.get().get()) { BOPAlgo_MakerVolume mv; From 8583d0963fd8a6205ebc9d7f0d0de1e008a15cdb Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Thu, 4 Jun 2026 21:38:11 +0200 Subject: [PATCH 03/12] Make faceset duplicate loop detection respect inner/outer #8140 --- src/ifcgeom/kernels/opencascade/faceset_helper.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/ifcgeom/kernels/opencascade/faceset_helper.cpp b/src/ifcgeom/kernels/opencascade/faceset_helper.cpp index 2fd74cdf2d..54d4bfb8d7 100644 --- a/src/ifcgeom/kernels/opencascade/faceset_helper.cpp +++ b/src/ifcgeom/kernels/opencascade/faceset_helper.cpp @@ -154,7 +154,13 @@ IfcGeom::OpenCascadeKernel::faceset_helper::faceset_helper( typedef std::array edge_t; typedef std::set edge_set_t; - std::set edge_sets; + // When a single face fills an interior loop, their edge_sets (canonicalized edges) will be identical. + // We can differentiate in this scenario in two ways: + // - std::map retain the edge order from the bool passed to the loop_() lambda + // - std::pair with pair::first populated from external (FaceBound / OuterBound) + // The second has been found more reliable for typical models, because inner bound winding can be wrong. + // The can be made more resilient by first checking correct population of external and falling back to approach 1. + std::set> edge_sets; for (auto& loop : loops) { std::vector > segments; @@ -165,12 +171,12 @@ IfcGeom::OpenCascadeKernel::faceset_helper::faceset_helper( segments.push_back(std::make_pair(C, D)); }); - if (edge_sets.find(segment_set) != edge_sets.end()) { + if (edge_sets.find({loop->external.get_value_or(false), segment_set}) != edge_sets.end()) { duplicate_faces++; duplicates_.insert(loop->identity()); continue; } - edge_sets.insert(segment_set); + edge_sets.insert({loop->external.get_value_or(false), segment_set}); if (segments.size() >= 3) { for (auto& p : segments) { From 94fab271cddce33cffd640290eac5db029faa277 Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Thu, 4 Jun 2026 21:41:06 +0200 Subject: [PATCH 04/12] Check for empty result after BOPAlgo_MakerVolume and reset manifoldness state #8140 --- .../kernels/opencascade/OpenCascadeKernel.cpp | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/ifcgeom/kernels/opencascade/OpenCascadeKernel.cpp b/src/ifcgeom/kernels/opencascade/OpenCascadeKernel.cpp index 5284a26a5b..85125f092c 100644 --- a/src/ifcgeom/kernels/opencascade/OpenCascadeKernel.cpp +++ b/src/ifcgeom/kernels/opencascade/OpenCascadeKernel.cpp @@ -152,12 +152,23 @@ bool IfcGeom::OpenCascadeKernel::convert_openings(const IfcUtil::IfcBaseEntity* BOPAlgo_MakerVolume mv; mv.AddArgument(entity_part); mv.SetAvoidInternalShapes(true); + // mv.SetFuzzyValue(settings_.get().get()); + std::optional failure; try { mv.Perform(); - entity_part = mv.Shape(); - Logger::Warning("Sucessfully detected exterior volume to non-manifold first operand"); + auto entity_part_2 = mv.Shape(); + if (IfcGeom::util::count(entity_part_2, TopAbs_FACE) == 0) { + failure = "Empty result (no faces) for BOPAlgo_MakerVolume; original was " + std::to_string(IfcGeom::util::count(entity_part, TopAbs_FACE)); + } else { + is_manifold = util::is_manifold(entity_part_2); + Logger::Warning(std::string("Sucessfully detected exterior volume to non-manifold first operand; shape is now ") + (is_manifold ? std::string("manifold") : std::string("non-manifold"))); + entity_part = entity_part_2; + } } catch (const Standard_Failure& e) { - Logger::Warning("MakeVolume failed: " + std::string(e.GetMessageString()), entity); + failure.emplace(e.GetMessageString()); + } + if (failure) { + Logger::Warning("MakeVolume failed: " + *failure, entity); } } else { Logger::Warning("Non-manifold first operand, use --make-volume to try and make manifold"); From 1f2b20fd8604a7d225876e758672d79b15ab0f46 Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Thu, 4 Jun 2026 22:15:35 +0200 Subject: [PATCH 05/12] Fix --convert-back-units on transformation object #8137 --- src/ifcgeom/IfcGeomElement.h | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/ifcgeom/IfcGeomElement.h b/src/ifcgeom/IfcGeomElement.h index 0d7e179d7b..68542dd23c 100644 --- a/src/ifcgeom/IfcGeomElement.h +++ b/src/ifcgeom/IfcGeomElement.h @@ -35,13 +35,25 @@ namespace IfcGeom { class Transformation { private: ifcopenshell::geometry::Settings settings_; - ifcopenshell::geometry::taxonomy::matrix4::ptr matrix_; + ifcopenshell::geometry::taxonomy::matrix4::ptr matrix_, matrix_orig_units_; public: - Transformation(const ifcopenshell::geometry::Settings& settings, const ifcopenshell::geometry::taxonomy::matrix4::ptr& matrix) - : settings_(settings) - , matrix_(matrix) - {} + Transformation(const ifcopenshell::geometry::Settings& settings, const ifcopenshell::geometry::taxonomy::matrix4::ptr& matrix) + : settings_(settings), matrix_(matrix) + { + const bool convert = settings.get().get(); + auto unit_magnitude = settings.get().get(); + if (matrix_ && convert && unit_magnitude != 1.0) { + matrix_orig_units_ = ifcopenshell::geometry::taxonomy::make(*matrix); + // only multiple the translation components of the matrix with the unit magnitude, not the rotation/scaling components + matrix_orig_units_->components().col(3).head<3>() /= unit_magnitude; + } else { + matrix_orig_units_ = nullptr; + } + } const ifcopenshell::geometry::taxonomy::matrix4::ptr& data() const { + if (matrix_orig_units_) { + return matrix_orig_units_; + } if (matrix_) { return matrix_; } From 674ed36e4135ae245bd65f4c515550ac61722066 Mon Sep 17 00:00:00 2001 From: Bruno Postle Date: Thu, 4 Jun 2026 22:31:48 +0100 Subject: [PATCH 06/12] Fix HDF5 config-mode detection to use shared library when static is absent When HDF5 is found via its CMake config file, the code previously hardcoded the hdf5_cpp-static target. On distributions that ship only shared HDF5 (e.g. Fedora rawhide where the config file was added in a newer package), this caused a link failure. Now checks for hdf5_cpp-static, hdf5_cpp-shared, and hdf5::hdf5_cpp-shared in order, falling back to module-mode discovery. --- cmake/FindHDF5.cmake | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/cmake/FindHDF5.cmake b/cmake/FindHDF5.cmake index 399b1c9f13..3230336a7d 100644 --- a/cmake/FindHDF5.cmake +++ b/cmake/FindHDF5.cmake @@ -88,7 +88,15 @@ if(NOT HDF5_INCLUDE_DIR OR NOT HDF5_LIBRARY_DIR) mark_as_advanced(HDF5_DIR) if(HDF5_DIR) message(STATUS "HDF5: found config at '${HDF5_DIR}'.") - set(HDF5_LIBRARIES hdf5_cpp-static) + if(TARGET hdf5_cpp-static) + set(HDF5_LIBRARIES hdf5_cpp-static) + elseif(TARGET hdf5_cpp-shared) + set(HDF5_LIBRARIES hdf5_cpp-shared) + elseif(TARGET hdf5::hdf5_cpp-shared) + set(HDF5_LIBRARIES hdf5::hdf5_cpp-shared) + else() + find_package(HDF5 REQUIRED COMPONENTS CXX) + endif() else() # If it failed, still try to find as a module. # E.g. on Ubuntu `libhdf5-dev` doesn't provie hdf5-config.cmake. From bd264f1d855a9a7609337b358b7903948c4d15b6 Mon Sep 17 00:00:00 2001 From: Bruno Postle Date: Thu, 4 Jun 2026 22:23:19 +0100 Subject: [PATCH 07/12] Add missing standard library includes for self-sufficient headers Fixes builds with newer GCC/libstdc++ that no longer provide , , , , etc. transitively. Also disambiguates visit<> calls in taxonomy.h with the full namespace and casts the character value in IfcCharacterDecoder to uint32_t to silence ambiguous overload warnings. --- src/ifcgeom/kernels/opencascade/IfcGeomTree.h | 1 + .../kernels/opencascade/clash_utils.cpp | 2 ++ src/ifcgeom/mapping/mapping.h | 1 + src/ifcgeom/taxonomy.h | 21 ++++++++++++------- src/ifcparse/IfcBaseClass.h | 1 + src/ifcparse/IfcCharacterDecoder.cpp | 2 +- src/ifcparse/IfcEntityInstanceData.h | 3 +++ src/ifcparse/IfcFile.h | 1 + src/ifcparse/IfcSchema.h | 2 ++ src/ifcparse/aggregate_of_instance.h | 1 + src/ifcparse/rocksdb_map_adapter.h | 2 ++ src/ifcparse/storage.h | 2 ++ src/ifcparse/variantarray.h | 4 ++++ src/serializers/GltfSerializer.cpp | 2 ++ src/serializers/HdfSerializer.cpp | 1 + src/serializers/RocksDbSerializer.cpp | 3 +++ 16 files changed, 41 insertions(+), 8 deletions(-) diff --git a/src/ifcgeom/kernels/opencascade/IfcGeomTree.h b/src/ifcgeom/kernels/opencascade/IfcGeomTree.h index 46e0f06731..aa5ae456a4 100644 --- a/src/ifcgeom/kernels/opencascade/IfcGeomTree.h +++ b/src/ifcgeom/kernels/opencascade/IfcGeomTree.h @@ -48,6 +48,7 @@ #include #include #include +#include #include #include #include diff --git a/src/ifcgeom/kernels/opencascade/clash_utils.cpp b/src/ifcgeom/kernels/opencascade/clash_utils.cpp index d6050bf87c..cfc7687b73 100644 --- a/src/ifcgeom/kernels/opencascade/clash_utils.cpp +++ b/src/ifcgeom/kernels/opencascade/clash_utils.cpp @@ -1,5 +1,7 @@ #include "clash_utils.h" #include +#include +#include #define GU_CULLING_EPSILON_RAY_TRIANGLE FLT_EPSILON*FLT_EPSILON #define PX_MAX_F32 3.4028234663852885981170418348452e+38F diff --git a/src/ifcgeom/mapping/mapping.h b/src/ifcgeom/mapping/mapping.h index 054e0a7efc..a9fe31999b 100644 --- a/src/ifcgeom/mapping/mapping.h +++ b/src/ifcgeom/mapping/mapping.h @@ -7,6 +7,7 @@ #include "../../ifcparse/IfcLogger.h" #include +#include #define INCLUDE_SCHEMA(x) STRINGIFY(../../ifcparse/x.h) #include INCLUDE_SCHEMA(IfcSchema) diff --git a/src/ifcgeom/taxonomy.h b/src/ifcgeom/taxonomy.h index c1162eebb0..a1d0c89ad4 100644 --- a/src/ifcgeom/taxonomy.h +++ b/src/ifcgeom/taxonomy.h @@ -17,6 +17,13 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include #ifndef TAXONOMY_USE_UNIQUE_PTR #ifndef TAXONOMY_USE_NAKED_PTR @@ -1625,19 +1632,19 @@ typedef item const* ptr; // @todo Sad... now that we have templated collection members, // we can't generally use collection_base anymore as a cast target. if (auto s = std::dynamic_pointer_cast(i)) { - visit(s, fn); + ifcopenshell::geometry::visit(s, fn); } else if (auto s = std::dynamic_pointer_cast(i)) { - visit(s, fn); + ifcopenshell::geometry::visit(s, fn); } else if (auto s = std::dynamic_pointer_cast(i)) { - visit(s, fn); + ifcopenshell::geometry::visit(s, fn); } else if (auto s = std::dynamic_pointer_cast(i)) { - visit(s, fn); + ifcopenshell::geometry::visit(s, fn); } else if (auto s = std::dynamic_pointer_cast(i)) { - visit(s, fn); + ifcopenshell::geometry::visit(s, fn); } else if (auto s = std::dynamic_pointer_cast(i)) { - visit(s, fn); + ifcopenshell::geometry::visit(s, fn); } else if (auto s = std::dynamic_pointer_cast(i)) { - visit(s, fn); + ifcopenshell::geometry::visit(s, fn); } else { fn(i); diff --git a/src/ifcparse/IfcBaseClass.h b/src/ifcparse/IfcBaseClass.h index 5cab8289c3..d03e7b284e 100644 --- a/src/ifcparse/IfcBaseClass.h +++ b/src/ifcparse/IfcBaseClass.h @@ -27,6 +27,7 @@ #include "utils.h" #include +#include #include class aggregate_of_instance; diff --git a/src/ifcparse/IfcCharacterDecoder.cpp b/src/ifcparse/IfcCharacterDecoder.cpp index 71685dcedc..c036aa676d 100644 --- a/src/ifcparse/IfcCharacterDecoder.cpp +++ b/src/ifcparse/IfcCharacterDecoder.cpp @@ -201,7 +201,7 @@ namespace { if (character >= 0x20 && character <= 0x7e) { stream.put((char)character); } else { - stream << "\\u" << character; + stream << "\\u" << static_cast(character); } }); return stream.str(); diff --git a/src/ifcparse/IfcEntityInstanceData.h b/src/ifcparse/IfcEntityInstanceData.h index 04e7dcb0ad..f51345a25e 100644 --- a/src/ifcparse/IfcEntityInstanceData.h +++ b/src/ifcparse/IfcEntityInstanceData.h @@ -36,6 +36,9 @@ #endif +#include +#include + #include #include #include diff --git a/src/ifcparse/IfcFile.h b/src/ifcparse/IfcFile.h index 96685a88ee..1092bb5e4f 100644 --- a/src/ifcparse/IfcFile.h +++ b/src/ifcparse/IfcFile.h @@ -34,6 +34,7 @@ #include #include #include +#include #ifdef IFOPSH_WITH_ROCKSDB #include diff --git a/src/ifcparse/IfcSchema.h b/src/ifcparse/IfcSchema.h index 349a81532d..dce8a43348 100644 --- a/src/ifcparse/IfcSchema.h +++ b/src/ifcparse/IfcSchema.h @@ -25,7 +25,9 @@ #include #include #include +#include #include +#include #include #include diff --git a/src/ifcparse/aggregate_of_instance.h b/src/ifcparse/aggregate_of_instance.h index bcce94ce6e..f7e78552b3 100644 --- a/src/ifcparse/aggregate_of_instance.h +++ b/src/ifcparse/aggregate_of_instance.h @@ -26,6 +26,7 @@ #include #include #include +#include namespace IfcParse { class declaration; diff --git a/src/ifcparse/rocksdb_map_adapter.h b/src/ifcparse/rocksdb_map_adapter.h index da2346789f..25d830463b 100644 --- a/src/ifcparse/rocksdb_map_adapter.h +++ b/src/ifcparse/rocksdb_map_adapter.h @@ -30,6 +30,8 @@ #include #include #include +#include +#include template struct is_std_tuple : std::false_type {}; diff --git a/src/ifcparse/storage.h b/src/ifcparse/storage.h index 60378c982e..d0ae6f3cf2 100644 --- a/src/ifcparse/storage.h +++ b/src/ifcparse/storage.h @@ -25,6 +25,8 @@ namespace rocksdb { #include #include +#include +#include #include #include #include diff --git a/src/ifcparse/variantarray.h b/src/ifcparse/variantarray.h index 16e2659564..1b00774bf7 100644 --- a/src/ifcparse/variantarray.h +++ b/src/ifcparse/variantarray.h @@ -33,6 +33,10 @@ variant - which is the maximum size of its constituents - is reduced. #include #include #include +#include +#include +#include +#include #include "IfcException.h" diff --git a/src/serializers/GltfSerializer.cpp b/src/serializers/GltfSerializer.cpp index 3627106f42..55ebb1d52c 100644 --- a/src/serializers/GltfSerializer.cpp +++ b/src/serializers/GltfSerializer.cpp @@ -23,6 +23,8 @@ #include "../ifcparse/utils.h" +#include + #ifdef WITH_PROJ #include #endif diff --git a/src/serializers/HdfSerializer.cpp b/src/serializers/HdfSerializer.cpp index 2fceb0eec0..092413abae 100644 --- a/src/serializers/HdfSerializer.cpp +++ b/src/serializers/HdfSerializer.cpp @@ -34,6 +34,7 @@ #include #include #include +#include #ifdef USE_BINARY #define write_shape write_binary diff --git a/src/serializers/RocksDbSerializer.cpp b/src/serializers/RocksDbSerializer.cpp index f5e95e7a3b..2a6221b670 100644 --- a/src/serializers/RocksDbSerializer.cpp +++ b/src/serializers/RocksDbSerializer.cpp @@ -4,6 +4,9 @@ #include +#include +#include + #include "../ifcparse/IfcLogger.h" RocksDbSerializer::RocksDbSerializer(IfcParse::IfcFile* file, const std::string& rocksdb_filename) From 9d956f18b78b87373445fb5628d21acc2329de9f Mon Sep 17 00:00:00 2001 From: Bruno Postle Date: Thu, 4 Jun 2026 22:25:30 +0100 Subject: [PATCH 08/12] Fix CGAL 6.x build: add Point_d_4d_Less comparator for std::map CGAL 6.x deleted operator< from Point_d, so std::map no longer compiles. Adds a custom lexicographic comparator and updates the three affected maps in snap_halfspaces and snap_halfspaces_2. --- .../kernels/cgal/nef_to_halfspace_tree.h | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/ifcgeom/kernels/cgal/nef_to_halfspace_tree.h b/src/ifcgeom/kernels/cgal/nef_to_halfspace_tree.h index fa410a84f9..c35d96d041 100644 --- a/src/ifcgeom/kernels/cgal/nef_to_halfspace_tree.h +++ b/src/ifcgeom/kernels/cgal/nef_to_halfspace_tree.h @@ -116,6 +116,18 @@ template using plane_map = std::map>; // using plane_map = std::unordered_map>; +// Lexicographic comparator for CGAL Point_d (operator< is deleted in CGAL 6.x) +struct Point_d_4d_Less { + using Point_d = CGAL::Epick_d>::Point_d; + bool operator()(const Point_d& a, const Point_d& b) const { + for (int i = 0; i < 4; ++i) { + if (a[i] < b[i]) return true; + if (b[i] < a[i]) return false; + } + return false; + } +}; + // Snap halfspace planes // search_radius: max cartesian distance in plane equation parameters as 4d points in space template @@ -131,8 +143,8 @@ plane_map snap_halfspaces(const std::list>& planes plane_map result; - std::map> neighbours; - std::map>> originals; + std::map, Point_d_4d_Less> neighbours; + std::map>, Point_d_4d_Less> originals; std::vector planes_as_point; for (auto& p : planes) { @@ -205,7 +217,7 @@ plane_map snap_halfspaces_2(const std::list>& plan plane_map result; std::vector planes_as_point; - std::map> normalized_to_original; + std::map, Point_d_4d_Less> normalized_to_original; for (auto& p : planes_fixed) { // @todo can we skip normalization (simply divide by largest component perhaps) From eacff93945ca73c64c5432b61ad3f8befc67ce51 Mon Sep 17 00:00:00 2001 From: Bruno Postle Date: Fri, 5 Jun 2026 08:17:29 +0100 Subject: [PATCH 09/12] Use std::lexicographical_compare in Point_d_4d_Less --- src/ifcgeom/kernels/cgal/nef_to_halfspace_tree.h | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/ifcgeom/kernels/cgal/nef_to_halfspace_tree.h b/src/ifcgeom/kernels/cgal/nef_to_halfspace_tree.h index c35d96d041..368e49a2d2 100644 --- a/src/ifcgeom/kernels/cgal/nef_to_halfspace_tree.h +++ b/src/ifcgeom/kernels/cgal/nef_to_halfspace_tree.h @@ -120,11 +120,9 @@ using plane_map = std::map>::Point_d; bool operator()(const Point_d& a, const Point_d& b) const { - for (int i = 0; i < 4; ++i) { - if (a[i] < b[i]) return true; - if (b[i] < a[i]) return false; - } - return false; + return std::lexicographical_compare( + a.cartesian_begin(), a.cartesian_end(), + b.cartesian_begin(), b.cartesian_end()); } }; From 365be8fb52b0d2ae1521f615142cc473bd047af1 Mon Sep 17 00:00:00 2001 From: Bruno Postle Date: Thu, 4 Jun 2026 22:31:02 +0100 Subject: [PATCH 10/12] Support RocksDB shared library and new unique_ptr DB::Open API Some distributions (e.g. Fedora) ship only a shared RocksDB that exports RocksDB::rocksdb-shared rather than RocksDB::rocksdb. The CMake target selection now falls back to the shared target when the static one is absent. Newer RocksDB also changed DB::Open and DB::OpenForReadOnly to take std::unique_ptr* instead of DB**. IfcFile.cpp uses SFINAE tag dispatch to build against both old and new APIs without version detection. --- cmake/CMakeLists.txt | 10 +++++++--- src/ifcparse/IfcFile.cpp | 30 ++++++++++++++++++++++++++++-- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/cmake/CMakeLists.txt b/cmake/CMakeLists.txt index 917e66d192..baafc5c459 100644 --- a/cmake/CMakeLists.txt +++ b/cmake/CMakeLists.txt @@ -258,10 +258,14 @@ if(WITH_ROCKSDB) set(ROCKSDB_LIBRARIES "IFCOPENSHELL_RocksDB") target_compile_definitions(IFCOPENSHELL_RocksDB INTERFACE IFOPSH_WITH_ROCKSDB) set(SWIG_DEFINES ${SWIG_DEFINES} -DIFOPSH_WITH_ROCKSDB) - # Shared binaries for `rocksdb` only support limited API (only `c.h`), but we use `db.h` API. - # So rocksdb supported only as a static library. # See https://github.com/facebook/rocksdb/issues/981. - target_link_libraries(IFCOPENSHELL_RocksDB INTERFACE RocksDB::rocksdb) + if(TARGET RocksDB::rocksdb) + target_link_libraries(IFCOPENSHELL_RocksDB INTERFACE RocksDB::rocksdb) + elseif(TARGET RocksDB::rocksdb-shared) + target_link_libraries(IFCOPENSHELL_RocksDB INTERFACE RocksDB::rocksdb-shared) + else() + message(FATAL_ERROR "RocksDB found but neither RocksDB::rocksdb nor RocksDB::rocksdb-shared target exists") + endif() if(WITH_ZSTD) # @todo do we actually need the zstd include dir or rather just pass diff --git a/src/ifcparse/IfcFile.cpp b/src/ifcparse/IfcFile.cpp index 9e26b24442..06521d5565 100644 --- a/src/ifcparse/IfcFile.cpp +++ b/src/ifcparse/IfcFile.cpp @@ -7,6 +7,8 @@ #endif #include +#include +#include #include #include @@ -410,6 +412,26 @@ IfcUtil::IfcBaseClass* IfcParse::impl::rocks_db_file_storage::assert_existance(s } namespace { +#ifdef IFOPSH_WITH_ROCKSDB + // Newer RocksDB releases (e.g. the one shipped by Fedora rawhide) changed + // DB::Open / DB::OpenForReadOnly to take a std::unique_ptr* and removed + // the raw DB** overloads. These helpers select whichever signature the + // installed RocksDB exposes, so the code builds against both old and new + // headers. The int/long tag prefers the unique_ptr form when both exist. + template + auto rocksdb_open(Fn&& open, rocksdb::DB*& db, int) + -> decltype(open(std::declval*>())) { + std::unique_ptr owned; + auto status = open(&owned); + db = owned.release(); + return status; + } + template + auto rocksdb_open(Fn&& open, rocksdb::DB*& db, long) + -> decltype(open(std::declval())) { + return open(&db); + } +#endif rocksdb::DB* init_db(const std::string& filepath, bool readonly) { rocksdb::DB* db = nullptr; #ifdef IFOPSH_WITH_ROCKSDB @@ -446,9 +468,13 @@ namespace { rocksdb::Status status; if (readonly) { - status = rocksdb::DB::OpenForReadOnly(options, filepath, &db); + status = rocksdb_open([&](auto* dbptr) -> decltype(rocksdb::DB::OpenForReadOnly(options, filepath, dbptr)) { + return rocksdb::DB::OpenForReadOnly(options, filepath, dbptr); + }, db, 0); } else { - status = rocksdb::DB::Open(options, filepath, &db); + status = rocksdb_open([&](auto* dbptr) -> decltype(rocksdb::DB::Open(options, filepath, dbptr)) { + return rocksdb::DB::Open(options, filepath, dbptr); + }, db, 0); } if (!status.ok()) { return nullptr; From 24a241addc559ddd4669c6e8b6b755a3d94d2152 Mon Sep 17 00:00:00 2001 From: Bruno Postle Date: Fri, 5 Jun 2026 08:12:55 +0100 Subject: [PATCH 11/12] Use version preprocessor guards for RocksDB unique_ptr API, retain unique_ptr internally --- src/ifcparse/IfcFile.cpp | 59 ++++++++++++++++------------------------ src/ifcparse/storage.h | 3 +- 2 files changed, 26 insertions(+), 36 deletions(-) diff --git a/src/ifcparse/IfcFile.cpp b/src/ifcparse/IfcFile.cpp index 06521d5565..a1cd77e437 100644 --- a/src/ifcparse/IfcFile.cpp +++ b/src/ifcparse/IfcFile.cpp @@ -4,6 +4,7 @@ #ifdef IFOPSH_WITH_ROCKSDB #include #include +#include #endif #include @@ -412,30 +413,8 @@ IfcUtil::IfcBaseClass* IfcParse::impl::rocks_db_file_storage::assert_existance(s } namespace { + std::unique_ptr init_db(const std::string& filepath, bool readonly) { #ifdef IFOPSH_WITH_ROCKSDB - // Newer RocksDB releases (e.g. the one shipped by Fedora rawhide) changed - // DB::Open / DB::OpenForReadOnly to take a std::unique_ptr* and removed - // the raw DB** overloads. These helpers select whichever signature the - // installed RocksDB exposes, so the code builds against both old and new - // headers. The int/long tag prefers the unique_ptr form when both exist. - template - auto rocksdb_open(Fn&& open, rocksdb::DB*& db, int) - -> decltype(open(std::declval*>())) { - std::unique_ptr owned; - auto status = open(&owned); - db = owned.release(); - return status; - } - template - auto rocksdb_open(Fn&& open, rocksdb::DB*& db, long) - -> decltype(open(std::declval())) { - return open(&db); - } -#endif - rocksdb::DB* init_db(const std::string& filepath, bool readonly) { - rocksdb::DB* db = nullptr; -#ifdef IFOPSH_WITH_ROCKSDB - rocksdb::Options options; // options.disable_auto_compactions = true; options.create_if_missing = true; @@ -467,20 +446,31 @@ namespace { options.table_factory.reset(rocksdb::NewBlockBasedTableFactory(tbo)); rocksdb::Status status; + std::unique_ptr db; if (readonly) { - status = rocksdb_open([&](auto* dbptr) -> decltype(rocksdb::DB::OpenForReadOnly(options, filepath, dbptr)) { - return rocksdb::DB::OpenForReadOnly(options, filepath, dbptr); - }, db, 0); +#if ROCKSDB_MAJOR > 9 || (ROCKSDB_MAJOR == 9 && ROCKSDB_MINOR >= 11) + status = rocksdb::DB::OpenForReadOnly(options, filepath, &db); +#else + rocksdb::DB* raw = nullptr; + status = rocksdb::DB::OpenForReadOnly(options, filepath, &raw); + db.reset(raw); +#endif } else { - status = rocksdb_open([&](auto* dbptr) -> decltype(rocksdb::DB::Open(options, filepath, dbptr)) { - return rocksdb::DB::Open(options, filepath, dbptr); - }, db, 0); +#if ROCKSDB_MAJOR > 9 || (ROCKSDB_MAJOR == 9 && ROCKSDB_MINOR >= 11) + status = rocksdb::DB::Open(options, filepath, &db); +#else + rocksdb::DB* raw = nullptr; + status = rocksdb::DB::Open(options, filepath, &raw); + db.reset(raw); +#endif } if (!status.ok()) { return nullptr; } -#endif // IFOPSH_WITH_ROCKSDB# return db; +#else + return nullptr; +#endif } } @@ -489,12 +479,12 @@ IfcParse::impl::rocks_db_file_storage::rocks_db_file_storage(const std::string& : file(ffile) , db(init_db(filepath, readonly)) // @todo streaming serializer does not populate the byguid map - , byguid_internal_(db, "g|") + , byguid_internal_(db.get(), "g|") , byguid_(&byguid_internal_, [this](size_t v) { return assert_existance(v, entityinstance_ref); }, [](IfcUtil::IfcBaseClass* v) { return v->identity(); }) - , instance_ids_(db, "i|") + , instance_ids_(db.get(), "i|") , instance_by_name_(&instance_ids_, [this](size_t v) { return assert_existance(v, entityinstance_ref); }) - , bytype_(db, "t|") - , byref_excl_(db, "v|") + , bytype_(db.get(), "t|") + , byref_excl_(db.get(), "v|") // @todo by_identity is probably not correct here, this mapping is Name -> Identity, so Fn should have access to full pair? // , byidentity_(&byid_, [this](size_t v) { return assert_existance(v, by_identity); }, [](IfcUtil::IfcBaseClass* v) { return v->identity(); }) { @@ -517,7 +507,6 @@ IfcParse::impl::rocks_db_file_storage::~rocks_db_file_storage() assert(s.ok()); db->Close(); - delete db; #endif } diff --git a/src/ifcparse/storage.h b/src/ifcparse/storage.h index d0ae6f3cf2..39e5139040 100644 --- a/src/ifcparse/storage.h +++ b/src/ifcparse/storage.h @@ -31,6 +31,7 @@ namespace rocksdb { #include #include #include +#include #ifndef SWIG @@ -308,7 +309,7 @@ namespace IfcParse { class IFC_PARSE_API rocks_db_file_storage { public: - rocksdb::DB* db; + std::unique_ptr db; rocksdb::WriteOptions wopts; rocksdb::ReadOptions ropts; IfcParse::IfcFile* file; From 06d99feeea34ef71688bc6c29cf41409ca01d9df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Fri, 5 Jun 2026 18:29:19 -0300 Subject: [PATCH 12/12] Add no headless test for Bonsai Snap Target. --- src/bonsai/test/files/snap-target.ifc | 91 +++++++++++++++++++++++++++ src/bonsai/test/modal/test_modal.py | 64 +++++++++++++++++++ 2 files changed, 155 insertions(+) create mode 100644 src/bonsai/test/files/snap-target.ifc diff --git a/src/bonsai/test/files/snap-target.ifc b/src/bonsai/test/files/snap-target.ifc new file mode 100644 index 0000000000..0b7132dece --- /dev/null +++ b/src/bonsai/test/files/snap-target.ifc @@ -0,0 +1,91 @@ +ISO-10303-21; +HEADER; +FILE_DESCRIPTION(('ViewDefinition[DesignTransferView]'),'2;1'); +FILE_NAME('snap-target.ifc','2026-06-05T17:45:10-03:00',(''),(''),'IfcOpenShell 0.0.0','Bonsai 0.8.6-alpha260605-24a241a','Nobody'); +FILE_SCHEMA(('IFC4')); +ENDSEC; +DATA; +#1=IFCPROJECT('2pZygwkcb1Au$5kgwmW6ZC',$,'My Project',$,$,$,$,(#10,#22),#5); +#2=IFCSIUNIT(*,.LENGTHUNIT.,.MILLI.,.METRE.); +#3=IFCSIUNIT(*,.AREAUNIT.,$,.SQUARE_METRE.); +#4=IFCSIUNIT(*,.VOLUMEUNIT.,$,.CUBIC_METRE.); +#5=IFCUNITASSIGNMENT((#4,#2,#3)); +#6=IFCCARTESIANPOINT((0.,0.,0.)); +#7=IFCDIRECTION((0.,0.,1.)); +#8=IFCDIRECTION((1.,0.,0.)); +#9=IFCAXIS2PLACEMENT3D(#6,#7,#8); +#10=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.E-05,#9,$); +#11=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Body','Model',*,*,*,*,#10,$,.MODEL_VIEW.,$); +#12=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Axis','Model',*,*,*,*,#10,$,.GRAPH_VIEW.,$); +#13=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Box','Model',*,*,*,*,#10,$,.MODEL_VIEW.,$); +#14=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Annotation','Model',*,*,*,*,#10,$,.SECTION_VIEW.,$); +#15=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Annotation','Model',*,*,*,*,#10,$,.ELEVATION_VIEW.,$); +#16=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Annotation','Model',*,*,*,*,#10,$,.MODEL_VIEW.,$); +#17=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Annotation','Model',*,*,*,*,#10,$,.PLAN_VIEW.,$); +#18=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Profile','Model',*,*,*,*,#10,$,.ELEVATION_VIEW.,$); +#19=IFCCARTESIANPOINT((0.,0.)); +#20=IFCDIRECTION((1.,0.)); +#21=IFCAXIS2PLACEMENT2D(#19,#20); +#22=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Plan',2,1.E-05,#21,$); +#23=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Axis','Plan',*,*,*,*,#22,$,.GRAPH_VIEW.,$); +#24=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Body','Plan',*,*,*,*,#22,$,.PLAN_VIEW.,$); +#25=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Annotation','Plan',*,*,*,*,#22,$,.PLAN_VIEW.,$); +#26=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Annotation','Plan',*,*,*,*,#22,$,.REFLECTED_PLAN_VIEW.,$); +#27=IFCSITE('1Lvnr1aSn2OP70jevISY_C',$,'My Site',$,$,#50,$,$,$,$,$,$,$,$); +#33=IFCBUILDING('1NaNtC8Wf6YPU7CZRG8Z8Q',$,'My Building',$,$,#56,$,$,$,$,$,$); +#39=IFCBUILDINGSTOREY('1k9kfaMOr2cvyNvxvoll27',$,'My Storey',$,$,#62,$,$,$,$); +#45=IFCRELAGGREGATES('1plJGwDIHDzwuc7i7gcQQD',$,$,$,#1,(#27)); +#46=IFCCARTESIANPOINT((0.,0.,0.)); +#47=IFCDIRECTION((0.,0.,1.)); +#48=IFCDIRECTION((1.,0.,0.)); +#49=IFCAXIS2PLACEMENT3D(#46,#47,#48); +#50=IFCLOCALPLACEMENT($,#49); +#51=IFCRELAGGREGATES('13KnzD8ZT8bAvAyOQAWiUj',$,$,$,#27,(#33)); +#52=IFCCARTESIANPOINT((0.,0.,0.)); +#53=IFCDIRECTION((0.,0.,1.)); +#54=IFCDIRECTION((1.,0.,0.)); +#55=IFCAXIS2PLACEMENT3D(#52,#53,#54); +#56=IFCLOCALPLACEMENT(#50,#55); +#57=IFCRELAGGREGATES('2Hf4WQLJvE4O43wq$0AZeu',$,$,$,#33,(#39)); +#58=IFCCARTESIANPOINT((0.,0.,0.)); +#59=IFCDIRECTION((0.,0.,1.)); +#60=IFCDIRECTION((1.,0.,0.)); +#61=IFCAXIS2PLACEMENT3D(#58,#59,#60); +#62=IFCLOCALPLACEMENT(#56,#61); +#63=IFCPROPERTYSINGLEVALUE('Symbol',$,IFCLABEL('setout-point'),$); +#64=IFCPROPERTYSET('27lmSbeAXC08EWkEq8XdUG',$,'EPset_Annotation',$,(#63)); +#65=IFCTYPEPRODUCT('0UrP0fLdD5OwzwD41aRKao',$,'SETOUT-POINT',$,'IfcAnnotation/SYMBOL',(#64),$,$); +#66=IFCCARTESIANPOINTLIST3D(((-8999.9990234375,-8999.9990234375,0.),(8999.9990234375,-8999.9990234375,0.),(-8999.9990234375,8999.9990234375,0.),(8999.9990234375,8999.9990234375,0.))); +#67=IFCINDEXEDPOLYGONALFACE((1,2,4,3)); +#68=IFCPOLYGONALFACESET(#66,$,(#67),$); +#69=IFCSHAPEREPRESENTATION(#11,'Body','Tessellation',(#68)); +#70=IFCBUILDINGELEMENTPROXY('3Avrn7zrPBiA7RUh91ENg9',$,'Plane',$,$,#142,#72,$,.COMPLEX.); +#71=IFCRELCONTAINEDINSPATIALSTRUCTURE('0ftVF1Vf1Dmg$p62mtpWVF',$,$,$,(#123,#70,#108),#39); +#72=IFCPRODUCTDEFINITIONSHAPE($,$,(#69)); +#108=IFCBUILDINGELEMENTPROXY('1lwURx5YX5WAXD5ExqQt31',$,'Plane',$,$,#122,#117,$,.COMPLEX.); +#114=IFCCARTESIANPOINTLIST3D(((-5999.99951171875,-2999.99975585938,0.),(5999.99951171875,-2999.99975585938,0.))); +#115=IFCINDEXEDPOLYCURVE(#114,(IFCLINEINDEX((1,2))),$); +#116=IFCSHAPEREPRESENTATION(#11,'Body','Curve3D',(#115)); +#117=IFCPRODUCTDEFINITIONSHAPE($,$,(#116)); +#118=IFCCARTESIANPOINT((0.,0.,0.)); +#119=IFCDIRECTION((0.,0.,1.)); +#120=IFCDIRECTION((1.,0.,0.)); +#121=IFCAXIS2PLACEMENT3D(#118,#119,#120); +#122=IFCLOCALPLACEMENT(#62,#121); +#123=IFCBUILDINGELEMENTPROXY('2X4YMFxHjANvnkl1Hzqjlq',$,'Plane',$,$,#137,#132,$,.COMPLEX.); +#129=IFCCARTESIANPOINTLIST3D(((3000.,-5999.99951171875,0.),(2999.99951171875,5999.99951171875,0.))); +#130=IFCINDEXEDPOLYCURVE(#129,(IFCLINEINDEX((1,2))),$); +#131=IFCSHAPEREPRESENTATION(#11,'Body','Curve3D',(#130)); +#132=IFCPRODUCTDEFINITIONSHAPE($,$,(#131)); +#133=IFCCARTESIANPOINT((0.,0.,0.)); +#134=IFCDIRECTION((0.,0.,1.)); +#135=IFCDIRECTION((1.,0.,0.)); +#136=IFCAXIS2PLACEMENT3D(#133,#134,#135); +#137=IFCLOCALPLACEMENT(#62,#136); +#138=IFCCARTESIANPOINT((0.,0.,0.)); +#139=IFCDIRECTION((0.,0.,1.)); +#140=IFCDIRECTION((1.,0.,0.)); +#141=IFCAXIS2PLACEMENT3D(#138,#139,#140); +#142=IFCLOCALPLACEMENT(#62,#141); +ENDSEC; +END-ISO-10303-21; diff --git a/src/bonsai/test/modal/test_modal.py b/src/bonsai/test/modal/test_modal.py index 27e290ae3f..07ce8b90ed 100644 --- a/src/bonsai/test/modal/test_modal.py +++ b/src/bonsai/test/modal/test_modal.py @@ -296,6 +296,63 @@ def test_snap_far_from_origin(window): yield from preset_event_simulate(window, "RET", "TAP", x, y) yield "FINISHED" +def test_snap_targets(window): + yield from preset_event_simulate(window, "MOUSEMOVE", "NOTHING", 0, 0) + area, region = get_area_and_region(window) + x = round(area.width * 0.44 + area.x) + y = round(area.height * 0.73 + area.y) + + yield from preset_event_simulate(window, "ESC", "TAP", x, y) + + options = [] + props = tool.Snap.get_snap_props() + try: + annotations = props.__annotations__ + except AttributeError: + annotations = type(props).__annotations__ + for prop in annotations.keys(): + if getattr(props, prop): + options.append((prop, props.rna_type.properties[prop].name)) + + for prop, name in options: + any(setattr(props, prop2, prop2 == prop) for prop2, _ in options) # set prop to true and others to false + measure_settings = tool.Project.get_measure_tool_settings() + measure_settings.measurement_type = "POLYLINE" + for obj in tool.Blender.get_selected_objects(): + obj.select_set(False) + with bpy.context.temp_override(area=area, region=region, space_data=area.spaces[0]): + bpy.ops.bim.measure_tool("INVOKE_DEFAULT", measure_type="POLYLINE") + snap_types = [] + yield from preset_event_simulate(window, "MOUSEMOVE", "NOTHING", x, y) + yield from preset_event_simulate(window, "LEFTMOUSE", "TAP", x, y) + snap_type = tool.Model.get_polyline_props().snap_mouse_point[0].snap_type + snap_types.append(snap_type) + + new_x = x + 200 + new_y = y - 55 + yield from preset_event_simulate(window, "MOUSEMOVE", "NOTHING", new_x, new_y) + yield from preset_event_simulate(window, "MOUSEMOVE", "NOTHING", new_x, new_y) + yield from preset_event_simulate(window, "MOUSEMOVE", "NOTHING", new_x, new_y) + yield from preset_event_simulate(window, "MOUSEMOVE", "NOTHING", new_x, new_y) + yield from preset_event_simulate(window, "LEFTMOUSE", "TAP", x, y) + snap_type = tool.Model.get_polyline_props().snap_mouse_point[0].snap_type + snap_types.append(snap_type) + + new_x = x + 130 + new_y = y - 358 + yield from preset_event_simulate(window, "MOUSEMOVE", "NOTHING", new_x, new_y) + yield from preset_event_simulate(window, "MOUSEMOVE", "NOTHING", new_x, new_y) + yield from preset_event_simulate(window, "MOUSEMOVE", "NOTHING", new_x, new_y) + yield from preset_event_simulate(window, "MOUSEMOVE", "NOTHING", new_x, new_y) + yield from preset_event_simulate(window, "LEFTMOUSE", "TAP", x, y) + snap_type = tool.Model.get_polyline_props().snap_mouse_point[0].snap_type + snap_types.append(snap_type) + + yield from preset_event_simulate(window, "ESC", "TAP", x, y) + assert_msg = f"{name} should be in snap_types: {snap_types}" + assert name in snap_types + _assert_pass(assert_msg) + def test_draw_polyline_wall(window, x, y): yield from preset_event_simulate(window, "ESC", "TAP", x, y) area, region = get_area_and_region(window) @@ -358,6 +415,13 @@ def run_tests(): lambda w=window: test_snap_in_xray_mode(w), lambda w=window: test_snap_far_from_origin(w), ] + elif module_name == "snap-target": + filepath = f"./test/files/snap-target.ifc" + bpy.ops.bim.load_project(filepath=filepath) + window = _get_valid_window() + test_queue = [ + lambda w=window: test_snap_targets(w), + ] else: cleanup()