From 67451a812b67db1ff1c4f1e43b6675516c1578fb Mon Sep 17 00:00:00 2001 From: Petru Conduraru Date: Sun, 19 Jul 2026 22:00:17 +0300 Subject: [PATCH] ifcgeom: add bbox-substitution-threshold setting to swap overly dense geometry for bounding boxes Implements the approach described in issue 2639: substitute element geometry with its bounding box when the vertex density (vertices per cubic unit of bounding box volume) exceeds a user supplied threshold. This gives a large speedup on models containing overly detailed manufacturer models (e.g. sensors, sanitary terminals) whose exact shape is irrelevant for the output, most notably in the n-squared hidden line removal of the SVG serializer. The dormant substitute_with_box_based_on_density helper from the CGAL branch port is reworked to operate per representation item so that per item placements remain correct, to skip items where a box would not reduce complexity, and to guard against zero bounding box volume. It is wired into Converter::create_brep_for_representation_and_product behind the new optional setting, which is off by default and exposed automatically through IfcConvert and the Python settings interface. The opaque bounding box exchanged through the bounding_box(void*&) and set_box(void*) virtuals is changed from a kernel specific type to a plain double[6] so the caller can own and free it, and the OpenCASCADE implementations of these two methods, previously stubs that threw, are provided using Bnd_Box and BRepPrimAPI_MakeBox. The CGAL set_box now also invalidates the cached nef representation. Generated with the assistance of an AI coding tool. --- src/ifcgeom/ConversionResult.h | 2 + src/ifcgeom/ConversionSettings.h | 11 ++++- src/ifcgeom/Converter.cpp | 40 ++++++++++++------ .../kernels/cgal/CgalConversionResult.cpp | 29 ++++++++++--- .../OpenCascadeConversionResult.cpp | 41 +++++++++++++++++++ .../opencascade/OpenCascadeConversionResult.h | 8 +--- .../docs/ifcopenshell/geometry_settings.rst | 15 +++++++ .../ifcopenshell/geom/main.py | 1 + 8 files changed, 121 insertions(+), 26 deletions(-) diff --git a/src/ifcgeom/ConversionResult.h b/src/ifcgeom/ConversionResult.h index 773c296c5f..8f0dcc39a2 100644 --- a/src/ifcgeom/ConversionResult.h +++ b/src/ifcgeom/ConversionResult.h @@ -464,6 +464,8 @@ namespace IfcGeom { virtual int num_faces() const = 0; // @todo choose one prototype + // b points to a double[6] of (xmin, ymin, zmin, xmax, ymax, zmax), allocated by the + // callee when passed as null and merged into otherwise; ownership stays with the caller virtual double bounding_box(void*&) const = 0; // @todo this must be something with a virtual dtor so that we can delete it. virtual std::pair, OpaqueCoordinate<3>> bounding_box() const = 0; diff --git a/src/ifcgeom/ConversionSettings.h b/src/ifcgeom/ConversionSettings.h index 621174cce2..6a5f9cd33e 100644 --- a/src/ifcgeom/ConversionSettings.h +++ b/src/ifcgeom/ConversionSettings.h @@ -415,6 +415,15 @@ namespace ifcopenshell { static constexpr bool defaultvalue = false; }; + struct BboxSubstitutionThreshold : public SettingBase { + static constexpr const char* const name = "bbox-substitution-threshold"; + static constexpr const char* const description = + "Substitutes the geometry of individual representation items with their axis-aligned " + "bounding box when the vertex density (mesh vertices per cubic unit of bounding box " + "volume) exceeds this threshold. Greatly reduces processing time on models with " + "overly detailed elements. Disabled when not provided."; + }; + struct SurfaceColour : public SettingBase { static constexpr const char* const name = "surface-colour"; static constexpr const char* const description = @@ -689,7 +698,7 @@ namespace ifcopenshell { }; class Settings : public SettingsContainer< - std::tuple + std::tuple > {}; } diff --git a/src/ifcgeom/Converter.cpp b/src/ifcgeom/Converter.cpp index 15a4584088..5947ad5c5f 100644 --- a/src/ifcgeom/Converter.cpp +++ b/src/ifcgeom/Converter.cpp @@ -2,6 +2,8 @@ #include "../ifcgeom/IfcGeomElement.h" +#include + using namespace ifcopenshell::geometry; ifcopenshell::geometry::Converter::Converter(std::unique_ptr&& geometry_library, IfcParse::IfcFile* file, ifcopenshell::geometry::Settings& s, Logger& logger) @@ -18,19 +20,27 @@ ifcopenshell::geometry::Converter::~Converter() { } namespace { - void substitute_with_box_based_on_density(Logger& logger, IfcGeom::ConversionResults& items, double& density) { - int nv = 0; - void* box = nullptr; - double volume = 0.; - for (auto& i : items) { - nv += i.Shape()->num_vertices(); - volume = i.Shape()->bounding_box(box); - } - density = nv / volume; - if (density > 1e5) { - items[0].Shape()->set_box(box); - items.erase(items.begin() + 1, items.end()); - logger.Notice("GEO", 30, "Substituted element with " + boost::lexical_cast(density) + " vertices / m3 with a bounding box"); + void substitute_with_box_based_on_density(Logger& logger, IfcGeom::ConversionResults& items, double threshold) { + for (auto& item : items) { + int nv = item.Shape()->num_vertices(); + if (nv <= 8) { + // substitution would not reduce complexity + continue; + } + void* box = nullptr; + double volume = item.Shape()->bounding_box(box); + if (box == nullptr) { + continue; + } + std::unique_ptr box_owner(static_cast(box)); + if (!(volume > 0.)) { + continue; + } + double density = nv / volume; + if (density > threshold) { + item.Shape()->set_box(box); + logger.Notice("GEO", 30, "Substituted item with " + boost::lexical_cast(density) + " vertices / m3 with a bounding box"); + } } } } @@ -49,6 +59,10 @@ IfcGeom::BRepElement* ifcopenshell::geometry::Converter::create_brep_for_represe return 0; } + if (settings_.get().has()) { + substitute_with_box_based_on_density(logger_, shapes, settings_.get().get()); + } + if (settings_.get().get()) { ifcopenshell::geometry::layerset_information layerinfo; std::vector neighbours; diff --git a/src/ifcgeom/kernels/cgal/CgalConversionResult.cpp b/src/ifcgeom/kernels/cgal/CgalConversionResult.cpp index 2fbdb0a307..954ed62589 100644 --- a/src/ifcgeom/kernels/cgal/CgalConversionResult.cpp +++ b/src/ifcgeom/kernels/cgal/CgalConversionResult.cpp @@ -9,6 +9,9 @@ #include "../../../ifcparse/IfcLogger.h" #include "../../../ifcgeom/IfcGeomRepresentation.h" +#include +#include + using IfcGeom::OpaqueNumber; using IfcGeom::OpaqueCoordinate; using IfcGeom::ConversionResultShape; @@ -618,10 +621,12 @@ void ifcopenshell::geometry::CgalShape::Serialize(const ifcopenshell::geometry:: #include double ifcopenshell::geometry::CgalShape::bounding_box(void *& b) const { + static const double inf = std::numeric_limits::infinity(); if (b == nullptr) { - b = new CGAL::Bbox_3; + b = new double[6]{ +inf, +inf, +inf, -inf, -inf, -inf }; } - auto& bb = (*((CGAL::Bbox_3*)b)); + double* box = static_cast(b); + CGAL::Bbox_3 bb; if (is_point()) { bb += point().bbox(); } else if (is_wire()) { @@ -631,7 +636,16 @@ double ifcopenshell::geometry::CgalShape::bounding_box(void *& b) const { } else { bb += CGAL::Polygon_mesh_processing::bbox(poly()); } - return (bb.xmax() - bb.xmin()) * (bb.ymax() - bb.ymin()) * (bb.zmax() - bb.zmin()); + box[0] = std::min(box[0], bb.xmin()); + box[1] = std::min(box[1], bb.ymin()); + box[2] = std::min(box[2], bb.zmin()); + box[3] = std::max(box[3], bb.xmax()); + box[4] = std::max(box[4], bb.ymax()); + box[5] = std::max(box[5], bb.zmax()); + if (box[0] > box[3] || box[1] > box[4] || box[2] > box[5]) { + return 0.; + } + return (box[3] - box[0]) * (box[4] - box[1]) * (box[5] - box[2]); } int ifcopenshell::geometry::CgalShape::num_vertices() const { @@ -645,10 +659,13 @@ int ifcopenshell::geometry::CgalShape::num_vertices() const { } void ifcopenshell::geometry::CgalShape::set_box(void * b) { - auto& bb = (*((CGAL::Bbox_3*)b)); - Kernel_::Point_3 lower(bb.xmin(), bb.ymin(), bb.zmin()); - Kernel_::Point_3 upper(bb.xmax(), bb.ymax(), bb.zmax()); + const double* box = static_cast(b); + Kernel_::Point_3 lower(box[0], box[1], box[2]); + Kernel_::Point_3 upper(box[3], box[4], box[5]); shape_ = ifcopenshell::geometry::utils::create_cube(lower, upper); +#ifndef IFOPSH_SIMPLE_KERNEL + nef_.reset(); +#endif } int ifcopenshell::geometry::CgalShape::surface_genus() const { diff --git a/src/ifcgeom/kernels/opencascade/OpenCascadeConversionResult.cpp b/src/ifcgeom/kernels/opencascade/OpenCascadeConversionResult.cpp index 6b6b403ba5..98413adebb 100644 --- a/src/ifcgeom/kernels/opencascade/OpenCascadeConversionResult.cpp +++ b/src/ifcgeom/kernels/opencascade/OpenCascadeConversionResult.cpp @@ -9,6 +9,10 @@ #include #include #include +#include +#include +#include +#include #include "OpenCascadeConversionResult.h" @@ -24,6 +28,7 @@ #include #include #include +#include #if OCC_VERSION_HEX >= 0x70600 #include @@ -374,6 +379,42 @@ int ifcopenshell::geometry::OpenCascadeShape::num_vertices() const return IfcGeom::util::count(shape_, TopAbs_VERTEX); } +double ifcopenshell::geometry::OpenCascadeShape::bounding_box(void*& b) const +{ + static const double inf = std::numeric_limits::infinity(); + if (b == nullptr) { + b = new double[6]{ +inf, +inf, +inf, -inf, -inf, -inf }; + } + double* box = static_cast(b); + Bnd_Box bnd; + BRepBndLib::Add(shape_, bnd, false); + if (!bnd.IsVoid()) { + double x1, y1, z1, x2, y2, z2; + bnd.Get(x1, y1, z1, x2, y2, z2); + box[0] = std::min(box[0], x1); + box[1] = std::min(box[1], y1); + box[2] = std::min(box[2], z1); + box[3] = std::max(box[3], x2); + box[4] = std::max(box[4], y2); + box[5] = std::max(box[5], z2); + } + if (box[0] > box[3] || box[1] > box[4] || box[2] > box[5]) { + return 0.; + } + return (box[3] - box[0]) * (box[4] - box[1]) * (box[5] - box[2]); +} + +void ifcopenshell::geometry::OpenCascadeShape::set_box(void* b) +{ + const double* box = static_cast(b); + double dims[3]; + for (int i = 0; i < 3; ++i) { + // guard against degenerate extents which BRepPrimAPI_MakeBox does not accept + dims[i] = std::max(box[3 + i] - box[i], ::Precision::Confusion() * 2); + } + shape_ = BRepPrimAPI_MakeBox(gp_Pnt(box[0], box[1], box[2]), dims[0], dims[1], dims[2]).Solid(); +} + int ifcopenshell::geometry::OpenCascadeShape::num_edges() const { return IfcGeom::util::count(shape_, TopAbs_EDGE); diff --git a/src/ifcgeom/kernels/opencascade/OpenCascadeConversionResult.h b/src/ifcgeom/kernels/opencascade/OpenCascadeConversionResult.h index 0dcd33c6a8..8a2e3fbd5a 100644 --- a/src/ifcgeom/kernels/opencascade/OpenCascadeConversionResult.h +++ b/src/ifcgeom/kernels/opencascade/OpenCascadeConversionResult.h @@ -59,13 +59,9 @@ namespace ifcopenshell { return new OpenCascadeShape(shape_); } - virtual double bounding_box(void*&) const { - throw std::runtime_error("Not implemented"); - } + virtual double bounding_box(void*&) const; - virtual void set_box(void*) { - throw std::runtime_error("Not implemented"); - } + virtual void set_box(void*); virtual int surface_genus() const; virtual bool is_manifold() const; diff --git a/src/ifcopenshell-python/docs/ifcopenshell/geometry_settings.rst b/src/ifcopenshell-python/docs/ifcopenshell/geometry_settings.rst index 6862088c6c..5060ced912 100644 --- a/src/ifcopenshell-python/docs/ifcopenshell/geometry_settings.rst +++ b/src/ifcopenshell-python/docs/ifcopenshell/geometry_settings.rst @@ -199,6 +199,21 @@ The interactive session below shows how with this setting enabled you will get a This is enabled by default for the IfcConvert serializers as they will not gracefully handle -1 material indices and allows users to quickly assign colours based on entity types in their modelling applications. +bbox-substitution-threshold +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + ++--------+-------------------------------------+---------+ +| Type | IfcConvert Option | Default | ++========+=====================================+=========+ +| DOUBLE | ``--bbox-substitution-threshold`` | | ++--------+-------------------------------------+---------+ + +Substitutes the geometry of individual representation items with their axis-aligned bounding box when the vertex density (mesh vertices per cubic unit of bounding box volume) exceeds this threshold. Greatly reduces processing time on models with overly detailed elements, for example manufacturer models of equipment inserted into a building model. Disabled when not provided. + +:: + + IfcConvert model.ifc model.svg --bbox-substitution-threshold 1000 + boolean-attempt-2d ^^^^^^^^^^^^^^^^^^ diff --git a/src/ifcopenshell-python/ifcopenshell/geom/main.py b/src/ifcopenshell-python/ifcopenshell/geom/main.py index c64d262e6f..b9e9399ccc 100644 --- a/src/ifcopenshell-python/ifcopenshell/geom/main.py +++ b/src/ifcopenshell-python/ifcopenshell/geom/main.py @@ -64,6 +64,7 @@ SETTING = Literal[ "angle-unit", "apply-default-materials", "apply-offset", + "bbox-substitution-threshold", "boolean-attempt-2d", "building-local-placement", "cache-shapes",