From 53881c48884f7e585860e905456c7a9fee6a358a Mon Sep 17 00:00:00 2001 From: Petru Conduraru Date: Sun, 19 Jul 2026 22:21:15 +0300 Subject: [PATCH] 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. --- .../kernels/opencascade/boolean_utils.cpp | 110 +++++++++++++++++- 1 file changed, 107 insertions(+), 3 deletions(-) diff --git a/src/ifcgeom/kernels/opencascade/boolean_utils.cpp b/src/ifcgeom/kernels/opencascade/boolean_utils.cpp index d104c7ff94..179b62c3d8 100644 --- a/src/ifcgeom/kernels/opencascade/boolean_utils.cpp +++ b/src/ifcgeom/kernels/opencascade/boolean_utils.cpp @@ -21,6 +21,8 @@ #include #include #include +#include +#include #include #include #include @@ -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 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& 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 b_trusted, b_with_repaired; + int num_repaired = 0, num_dropped = 0; + for (NCollection_List::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& 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