mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-31 17:06:29 +00:00
ifcgeom: salvage subtractions when OCCT refuses invalid boolean operands
When BOPAlgo refuses a cut wholesale (BOPAlgo_AlertBOPNotAllowed), for example because one of the subtraction operands is an open shell rather than a closed volume, all subtractions were previously discarded and the unclipped first operand was used (GEO152). Archicad models with degenerate clipping volumes (issue 2320) lose all wall clippings this way, since a single defective operand voids the good ones too. Instead, retry the operation with only the trustworthy operands: uniformly 3-dimensional, BRepCheck-valid and with positive volume. For the defective ones, first attempt to reconstruct a proper volume from their faces with BOPAlgo_MakerVolume, mirroring what --make-volume does for first operands. Invalid but 3-dimensional operands are also excluded, because inverted solids have been observed to classify the complete first operand as interior, silently emptying the result. The retried result passes through all existing acceptance gates unchanged (shape healing, BRepCheck, manifoldness, interference checks and the fuzziness retry ladder) plus a new gate specific to this path: a subtraction may never gain volume relative to the first operand. When every retry fails, behaviour is exactly as before: the first operand is used unmodified. On the issue 2320 storey, 28 of 31 previously unclipped elements now receive valid, strictly material-removing cuts; the remaining 3 fall back to the previous behaviour. Generated with the assistance of an AI coding tool.
This commit is contained in:
@@ -21,6 +21,8 @@
|
||||
#include <BRepPrimAPI_MakePrism.hxx>
|
||||
#include <BOPAlgo_PaveFiller.hxx>
|
||||
#include <BOPAlgo_Alerts.hxx>
|
||||
#include <BOPAlgo_MakerVolume.hxx>
|
||||
#include <BOPTools_AlgoTools.hxx>
|
||||
#include <ShapeFix_Shape.hxx>
|
||||
#include <BRepCheck_Analyzer.hxx>
|
||||
#include <BRepCheck.hxx>
|
||||
@@ -832,6 +834,56 @@ bool IfcGeom::util::points_on_planar_face_generator::operator()(gp_Pnt& p) {
|
||||
}
|
||||
|
||||
|
||||
#if OCC_VERSION_HEX >= 0x70400
|
||||
namespace {
|
||||
// A subtraction operand can only be trusted when it is unambiguously a volume:
|
||||
// uniformly 3-dimensional, valid and with positive volume. Open shells cause
|
||||
// BOPAlgo to refuse the entire operation, inverted or otherwise invalid solids
|
||||
// have been observed to classify the complete first operand as interior. #2320
|
||||
bool is_valid_cut_tool(const TopoDS_Shape& s) {
|
||||
Standard_Integer dim_min, dim_max;
|
||||
BOPTools_AlgoTools::Dimensions(s, dim_min, dim_max);
|
||||
if (dim_min != 3) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
BRepCheck_Analyzer analyzer(s);
|
||||
if (!analyzer.IsValid()) {
|
||||
return false;
|
||||
}
|
||||
return IfcGeom::util::shape_volume(s) > 0.;
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool reconstruct_volume(const TopoDS_Shape& s, double fuzziness, TopoDS_Shape& result) {
|
||||
try {
|
||||
NCollection_List<TopoDS_Shape> faces;
|
||||
for (TopExp_Explorer exp(s, TopAbs_FACE); exp.More(); exp.Next()) {
|
||||
faces.Append(exp.Current());
|
||||
}
|
||||
if (faces.IsEmpty()) {
|
||||
return false;
|
||||
}
|
||||
BOPAlgo_MakerVolume mv;
|
||||
mv.SetArguments(faces);
|
||||
mv.SetRunParallel(false);
|
||||
mv.SetIntersect(true);
|
||||
mv.SetFuzzyValue(fuzziness);
|
||||
mv.Perform();
|
||||
if (mv.HasErrors() || mv.Shape().IsNull() || !is_valid_cut_tool(mv.Shape())) {
|
||||
return false;
|
||||
}
|
||||
result = mv.Shape();
|
||||
return true;
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
bool IfcGeom::util::boolean_operation(const boolean_settings& settings, const TopoDS_Shape& a_input, const NCollection_List<TopoDS_Shape>& b_input, BOPAlgo_Operation op, TopoDS_Shape& result, double fuzziness) {
|
||||
using namespace std::string_literals;
|
||||
|
||||
@@ -1399,9 +1451,61 @@ bool IfcGeom::util::boolean_operation(const boolean_settings& settings, const To
|
||||
#if OCC_VERSION_HEX >= 0x70200
|
||||
|
||||
if (builder->HasError(STANDARD_TYPE(BOPAlgo_AlertBOPNotAllowed))) {
|
||||
settings.log().Error("GEO", 152, "Invalid operands. Using first operand");
|
||||
result = a;
|
||||
success = true;
|
||||
#if OCC_VERSION_HEX >= 0x70400
|
||||
if (op == BOPAlgo_CUT) {
|
||||
// The operation is refused e.g. when a subtraction operand is not a
|
||||
// closed volume. Retry with only the trustworthy volume operands so
|
||||
// that one defective operand does not void all subtractions. #2320
|
||||
NCollection_List<TopoDS_Shape> b_trusted, b_with_repaired;
|
||||
int num_repaired = 0, num_dropped = 0;
|
||||
for (NCollection_List<TopoDS_Shape>::Iterator it(b); it.More(); it.Next()) {
|
||||
if (is_valid_cut_tool(it.Value())) {
|
||||
b_trusted.Append(it.Value());
|
||||
b_with_repaired.Append(it.Value());
|
||||
} else {
|
||||
TopoDS_Shape repaired;
|
||||
if (reconstruct_volume(it.Value(), fuzz, repaired) ||
|
||||
reconstruct_volume(it.Value(), settings.precision, repaired) ||
|
||||
reconstruct_volume(it.Value(), settings.precision * 10., repaired))
|
||||
{
|
||||
b_with_repaired.Append(repaired);
|
||||
++num_repaired;
|
||||
} else {
|
||||
++num_dropped;
|
||||
}
|
||||
}
|
||||
}
|
||||
auto attempt_subset = [&](const NCollection_List<TopoDS_Shape>& ops) {
|
||||
TopoDS_Shape r;
|
||||
if (!boolean_operation(settings, a, ops, op, r, fuzziness)) {
|
||||
return false;
|
||||
}
|
||||
// A subtraction can never gain volume, reject reconstruction artifacts.
|
||||
const double va = shape_volume(a), vr = shape_volume(r);
|
||||
if (!(vr > 0.) || (va > 0. && vr > va * (1. + 1.e-6))) {
|
||||
settings.log().Notice("GEO", 408, "Retried subtraction rejected by volume check");
|
||||
return false;
|
||||
}
|
||||
result = r;
|
||||
return true;
|
||||
};
|
||||
if ((num_repaired + num_dropped) && b_with_repaired.Extent()) {
|
||||
settings.log().Warning("GEO", 406, "Invalid operands, retrying with " +
|
||||
std::to_string(b_with_repaired.Extent()) + "/" + std::to_string(b.Extent()) +
|
||||
" operands, of which " + std::to_string(num_repaired) + " reconstructed");
|
||||
success = attempt_subset(b_with_repaired);
|
||||
if (!success && num_repaired && b_trusted.Extent()) {
|
||||
settings.log().Notice("GEO", 407, "Retrying without reconstructed operands");
|
||||
success = attempt_subset(b_trusted);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (!success) {
|
||||
settings.log().Error("GEO", 152, "Invalid operands. Using first operand");
|
||||
result = a;
|
||||
success = true;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
Reference in New Issue
Block a user