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.
This commit is contained in:
Petru Conduraru
2026-07-19 22:00:17 +03:00
parent 89523999b3
commit 67451a812b
8 changed files with 121 additions and 26 deletions
+2
View File
@@ -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>, OpaqueCoordinate<3>> bounding_box() const = 0;
+10 -1
View File
@@ -415,6 +415,15 @@ namespace ifcopenshell {
static constexpr bool defaultvalue = false;
};
struct BboxSubstitutionThreshold : public SettingBase<BboxSubstitutionThreshold, double> {
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<SurfaceColour, bool> {
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<MesherLinearDeflection, MesherAngularDeflection, ReorientShells, LengthUnit, PlaneUnit, Precision, OutputDimensionality, LayersetFirst, DisableBooleanResult, NoWireIntersectionCheck, NoWireIntersectionTolerance, PrecisionFactor, DebugBooleanOperations, BooleanAttempt2d, SurfaceColour, WeldVertices, UseWorldCoords, UnifyShapes, UseMaterialNames, ConvertBackUnits, ContextIds, ContextTypes, ContextIdentifiers, IteratorOutput, DisableOpeningSubtractions, ApplyDefaultMaterials, DontEmitNormals, GenerateUvs, ApplyLayerSets, UseElementHierarchy, ValidateQuantities, EdgeArrows, BuildingLocalPlacement, SiteLocalPlacement, ForceSpaceTransparency, CircleSegments, CgalSmoothAngleDegrees, SvgRidgeAngleMinDegrees, SvgValleyAngleMinDegrees, SvgEmitFlushEdges, SvgUseEdgeClassification, SvgRenderCreaseEdges, SvgRenderSharpEdges, KeepBoundingBoxes, ComputeCurvature, FunctionStepType, FunctionStepParam, NoParallelMapping, PermissiveShapeReuse, ModelOffset, ModelRotation, TriangulationType, CgalEmitOriginalEdges, OcctNoCleanTriangulation, CacheShapes, DeferProcessingFirstElement, MaxOffset, MaxOffsetDeviation, ApplyOffset, MakeVolume>
std::tuple<MesherLinearDeflection, MesherAngularDeflection, ReorientShells, LengthUnit, PlaneUnit, Precision, OutputDimensionality, LayersetFirst, DisableBooleanResult, NoWireIntersectionCheck, NoWireIntersectionTolerance, PrecisionFactor, DebugBooleanOperations, BooleanAttempt2d, SurfaceColour, WeldVertices, UseWorldCoords, UnifyShapes, UseMaterialNames, ConvertBackUnits, ContextIds, ContextTypes, ContextIdentifiers, IteratorOutput, DisableOpeningSubtractions, ApplyDefaultMaterials, DontEmitNormals, GenerateUvs, ApplyLayerSets, UseElementHierarchy, ValidateQuantities, EdgeArrows, BuildingLocalPlacement, SiteLocalPlacement, ForceSpaceTransparency, CircleSegments, CgalSmoothAngleDegrees, SvgRidgeAngleMinDegrees, SvgValleyAngleMinDegrees, SvgEmitFlushEdges, SvgUseEdgeClassification, SvgRenderCreaseEdges, SvgRenderSharpEdges, KeepBoundingBoxes, BboxSubstitutionThreshold, ComputeCurvature, FunctionStepType, FunctionStepParam, NoParallelMapping, PermissiveShapeReuse, ModelOffset, ModelRotation, TriangulationType, CgalEmitOriginalEdges, OcctNoCleanTriangulation, CacheShapes, DeferProcessingFirstElement, MaxOffset, MaxOffsetDeviation, ApplyOffset, MakeVolume>
>
{};
}
+27 -13
View File
@@ -2,6 +2,8 @@
#include "../ifcgeom/IfcGeomElement.h"
#include <memory>
using namespace ifcopenshell::geometry;
ifcopenshell::geometry::Converter::Converter(std::unique_ptr<ifcopenshell::geometry::kernels::AbstractKernel>&& 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<std::string>(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<double[]> box_owner(static_cast<double*>(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<std::string>(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<ifcopenshell::geometry::settings::BboxSubstitutionThreshold>().has()) {
substitute_with_box_based_on_density(logger_, shapes, settings_.get<ifcopenshell::geometry::settings::BboxSubstitutionThreshold>().get());
}
if (settings_.get<ifcopenshell::geometry::settings::ApplyLayerSets>().get()) {
ifcopenshell::geometry::layerset_information layerinfo;
std::vector<ifcopenshell::geometry::endpoint_connection> neighbours;
@@ -9,6 +9,9 @@
#include "../../../ifcparse/IfcLogger.h"
#include "../../../ifcgeom/IfcGeomRepresentation.h"
#include <algorithm>
#include <limits>
using IfcGeom::OpaqueNumber;
using IfcGeom::OpaqueCoordinate;
using IfcGeom::ConversionResultShape;
@@ -618,10 +621,12 @@ void ifcopenshell::geometry::CgalShape::Serialize(const ifcopenshell::geometry::
#include <CGAL/Polygon_mesh_processing/bbox.h>
double ifcopenshell::geometry::CgalShape::bounding_box(void *& b) const {
static const double inf = std::numeric_limits<double>::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<double*>(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<const double*>(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 {
@@ -9,6 +9,10 @@
#include <BRepTools_WireExplorer.hxx>
#include <TopoDS_Compound.hxx>
#include <BRep_Builder.hxx>
#include <Bnd_Box.hxx>
#include <BRepBndLib.hxx>
#include <BRepPrimAPI_MakeBox.hxx>
#include <Precision.hxx>
#include "OpenCascadeConversionResult.h"
@@ -24,6 +28,7 @@
#include <unordered_map>
#include <tuple>
#include <algorithm>
#include <limits>
#if OCC_VERSION_HEX >= 0x70600
#include <TopTools_FormatVersion.hxx>
@@ -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<double>::infinity();
if (b == nullptr) {
b = new double[6]{ +inf, +inf, +inf, -inf, -inf, -inf };
}
double* box = static_cast<double*>(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<const double*>(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);
@@ -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;
@@ -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
^^^^^^^^^^^^^^^^^^
@@ -64,6 +64,7 @@ SETTING = Literal[
"angle-unit",
"apply-default-materials",
"apply-offset",
"bbox-substitution-threshold",
"boolean-attempt-2d",
"building-local-placement",
"cache-shapes",