Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 90 additions & 16 deletions src/ModelingAlgorithms/TKBO/BOPAlgo/BOPAlgo_RemoveFeatures.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@
#include <TopTools_ShapeMapHasher.hxx>
#include <NCollection_IndexedDataMap.hxx>

#include <algorithm>
#include <cmath>

//=======================================================================
// static methods declaration
//=======================================================================
Expand Down Expand Up @@ -374,7 +377,8 @@ class FillGap
//! Empty constructor
FillGap()
: myRunParallel(false),
myHasAdjacentFaces(false)
myHasAdjacentFaces(false),
myTrimDegenerated(false)
{
}

Expand Down Expand Up @@ -440,17 +444,82 @@ class FillGap
return;
}

// Extend the adjacent faces keeping the connection to the original faces
NCollection_IndexedDataMap<TopoDS_Shape, TopoDS_Shape, TopTools_ShapeMapHasher>
aFaceExtFaceMap;
ExtendAdjacentFaces(aMFAdjacent, aFaceExtFaceMap, aPS.Next());
if (!aPS.More())
// The extension length is derived from the size of the feature, which may be too
// small for the extended adjacent faces to reach each other. In that case trimming
// of the extended faces degenerates and the untrimmed extensions leak into the
// result, making the reconstruction unusable. Retry with a longer extension,
// keeping the result of the first attempt if none of them succeeds.
Bnd_Box aFeatureBox;
BRepBndLib::Add(myFeature, aFeatureBox);
const double aFeatureSize = sqrt(aFeatureBox.SquareExtent());

// The distance at which the adjacent surfaces meet is not related to the size of
// the feature - for faces meeting at a small angle it grows without bound - so the
// number of attempts is taken from the size of the solids the feature belongs to.
// The last attempt is then the first extension spanning the whole model; beyond
// that length the extended faces already cover it and doubling cannot bring them
// together anymore.
Bnd_Box aSolidsBox;
for (int i = 1; i <= mySolids.Extent(); ++i)
{
return;
BRepBndLib::Add(mySolids(i), aSolidsBox);
}
const double aSolidsSize = aSolidsBox.IsVoid() ? 0.0 : sqrt(aSolidsBox.SquareExtent());
const int aNbAttempts =
(aSolidsSize > aFeatureSize)
? std::max(THE_NB_EXTENSION_ATTEMPTS,
1 + static_cast<int>(std::ceil(std::log2(aSolidsSize / aFeatureSize))))
: THE_NB_EXTENSION_ATTEMPTS;

NCollection_IndexedDataMap<TopoDS_Shape,
NCollection_List<TopoDS_Shape>,
TopTools_ShapeMapHasher>
aFacesFirst;
occ::handle<BRepTools_History> aHistoryFirst;

// Trim the extended faces
TrimExtendedFaces(aFaceExtFaceMap, aPS.Next());
Message_ProgressScope aPSExt(aPS.Next(2), nullptr, aNbAttempts);
for (int anAttempt = 0; anAttempt < aNbAttempts; ++anAttempt)
{
myTrimDegenerated = false;
myFaces.Clear();
myHistory = new BRepTools_History();

Message_ProgressScope aPSAttempt(aPSExt.Next(), nullptr, 2);

// Extend the adjacent faces keeping the connection to the original faces
NCollection_IndexedDataMap<TopoDS_Shape, TopoDS_Shape, TopTools_ShapeMapHasher>
aFaceExtFaceMap;
ExtendAdjacentFaces(aMFAdjacent,
aFeatureSize * (1 << anAttempt),
aFaceExtFaceMap,
aPSAttempt.Next());
if (!aPSAttempt.More())
{
return;
}

// Trim the extended faces
TrimExtendedFaces(aFaceExtFaceMap, aPSAttempt.Next());

if (!myTrimDegenerated)
{
break;
}

if (anAttempt == 0)
{
aFacesFirst = myFaces;
aHistoryFirst = myHistory;
}
}

if (myTrimDegenerated)
{
// None of the attempts produced a properly trimmed reconstruction -
// keep the result of the first one.
myFaces = aFacesFirst;
myHistory = aHistoryFirst;
}
}
catch (Standard_Failure const&)
{
Expand Down Expand Up @@ -576,24 +645,19 @@ class FillGap
//! Extends the found adjacent faces and binds them to the original faces.
void ExtendAdjacentFaces(
const NCollection_IndexedMap<TopoDS_Shape, TopTools_ShapeMapHasher>& theMFAdjacent,
const double theExtLength,
NCollection_IndexedDataMap<TopoDS_Shape, TopoDS_Shape, TopTools_ShapeMapHasher>&
theFaceExtFaceMap,
const Message_ProgressRange& theRange)
{
// Get the extension value for the faces - half of the diagonal of bounding box of the feature
Bnd_Box aFeatureBox;
BRepBndLib::Add(myFeature, aFeatureBox);

const double anExtLength = sqrt(aFeatureBox.SquareExtent());

const int aNbFA = theMFAdjacent.Extent();
Message_ProgressScope aPS(theRange, "Extending adjacent faces", aNbFA);
for (int i = 1; i <= aNbFA && aPS.More(); ++i, aPS.Next())
{
const TopoDS_Face& aF = TopoDS::Face(theMFAdjacent(i));
// Extend the face
TopoDS_Face aFExt;
BRepLib::ExtendFace(aF, anExtLength, true, true, true, true, aFExt);
BRepLib::ExtendFace(aF, theExtLength, true, true, true, true, aFExt);
theFaceExtFaceMap.Add(aF, aFExt);
myHistory->AddModified(aF, aFExt);
}
Expand Down Expand Up @@ -835,6 +899,11 @@ class FillGap
}
else if (aLFTrimmed.IsEmpty())
{
// The extended face could not be trimmed by the bounds of the original face -
// most likely the extension is too small for the extended faces to intersect
// each other. Signal it, so that the extension can be retried with a greater
// length.
myTrimDegenerated = true;
// Use all splits, including those having the bounds of extended face
anExpF.ReInit();
for (; anExpF.More(); anExpF.Next())
Expand Down Expand Up @@ -888,7 +957,12 @@ class FillGap
NCollection_IndexedMap<TopoDS_Shape, TopTools_ShapeMapHasher> mySolids; //!< Solids participating in the feature removal
NCollection_IndexedDataMap<TopoDS_Shape, NCollection_List<TopoDS_Shape>, TopTools_ShapeMapHasher> myFaces; //!< Reconstructed adjacent faces
occ::handle<BRepTools_History> myHistory; //!< History of the adjacent faces reconstruction
bool myTrimDegenerated; //!< Flag to show that trimming of the extended faces has failed
// clang-format on

//! Least number of attempts to extend the adjacent faces, each one doubling the
//! extension length; more are made when the model is much larger than the feature
static constexpr int THE_NB_EXTENSION_ATTEMPTS = 3;
};

typedef NCollection_DynamicArray<FillGap> VectorOfFillGap;
Expand Down
148 changes: 148 additions & 0 deletions src/ModelingAlgorithms/TKBO/GTests/BRepAlgoAPI_Defeaturing_Test.cxx
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// Copyright (c) 2026 OPEN CASCADE SAS
//
// This file is part of Open CASCADE Technology software library.
//
// This library is free software; you can redistribute it and/or modify it under
// the terms of the GNU Lesser General Public License version 2.1 as published
// by the Free Software Foundation, with special exception defined in the file
// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT
// distribution for complete text of the license and disclaimer of any warranty.
//
// Alternatively, this file may be used under the terms of Open CASCADE
// commercial license or contractual agreement.

#include "BOPTest_Utilities.pxx"

#include <BRepAdaptor_Surface.hxx>
#include <BRepAlgoAPI_Defeaturing.hxx>
#include <BRepBuilderAPI_MakeFace.hxx>
#include <BRepBuilderAPI_MakePolygon.hxx>
#include <BRepCheck_Analyzer.hxx>
#include <BRepFilletAPI_MakeFillet.hxx>
#include <BRepPrimAPI_MakePrism.hxx>
#include <BRep_Tool.hxx>

#include <TopExp.hxx>
#include <TopTools_ShapeMapHasher.hxx>

namespace
{
typedef NCollection_IndexedMap<TopoDS_Shape, TopTools_ShapeMapHasher> ShapeMap;

const double THE_WEDGE_ANGLE = 15.0; // degrees between the two faces of the wedge
const double THE_WEDGE_LENGTH = 60.0;
const double THE_WEDGE_DEPTH = 5.0;
const double THE_ROUND_RADIUS = 5.0;

int CountFaces(const TopoDS_Shape& theShape)
{
ShapeMap aMap;
TopExp::MapShapes(theShape, TopAbs_FACE, aMap);
return aMap.Extent();
}

//! A wedge with a round on its sharp edge.
struct WedgeModel
{
TopoDS_Shape myWedge; //!< wedge with a sharp edge
TopoDS_Shape myRounded; //!< the same wedge with the edge rounded
TopoDS_Face myRound; //!< the round face of myRounded
};

//! Build a wedge of the given angle and put a round on its sharp edge.
//!
//! The round is short, so its bounding box is much smaller than the distance
//! from it to the sharp edge it replaces: the faces of the wedge have to be
//! extended by several times the size of the round before they meet each other
//! again. With the angle and the radius used here the round measures about 12,
//! while its tangency lines stand about 38 away from the edge.
WedgeModel MakeRoundedWedge()
{
const double aHalfAngle = 0.5 * THE_WEDGE_ANGLE * M_PI / 180.0;

BRepBuilderAPI_MakePolygon aProfile;
aProfile.Add(gp_Pnt(0.0, 0.0, 0.0));
aProfile.Add(gp_Pnt(THE_WEDGE_LENGTH, 0.0, 0.0));
aProfile.Add(gp_Pnt(THE_WEDGE_LENGTH, 0.0, THE_WEDGE_LENGTH * tan(2.0 * aHalfAngle)));
aProfile.Close();

WedgeModel aModel;
const TopoDS_Face aFace = BRepBuilderAPI_MakeFace(aProfile.Wire()).Face();
aModel.myWedge = BRepPrimAPI_MakePrism(aFace, gp_Vec(0.0, THE_WEDGE_DEPTH, 0.0)).Shape();

// The sharp edge of the wedge runs along Y through the origin.
TopoDS_Edge anEdge;
ShapeMap anEdges;
TopExp::MapShapes(aModel.myWedge, TopAbs_EDGE, anEdges);
for (int anIndex = 1; anIndex <= anEdges.Extent() && anEdge.IsNull(); ++anIndex)
{
const TopoDS_Edge& aCandidate = TopoDS::Edge(anEdges(anIndex));
TopoDS_Vertex aV1, aV2;
TopExp::Vertices(aCandidate, aV1, aV2);
const gp_Pnt aP1 = BRep_Tool::Pnt(aV1);
const gp_Pnt aP2 = BRep_Tool::Pnt(aV2);
if (aP1.X() < Precision::Confusion() && aP1.Z() < Precision::Confusion()
&& aP2.X() < Precision::Confusion() && aP2.Z() < Precision::Confusion())
{
anEdge = aCandidate;
}
}
if (anEdge.IsNull())
{
return aModel;
}

BRepFilletAPI_MakeFillet aFilletMaker(aModel.myWedge);
aFilletMaker.Add(THE_ROUND_RADIUS, anEdge);
aFilletMaker.Build();
if (!aFilletMaker.IsDone())
{
return aModel;
}
aModel.myRounded = aFilletMaker.Shape();

ShapeMap aFaces;
TopExp::MapShapes(aModel.myRounded, TopAbs_FACE, aFaces);
for (int anIndex = 1; anIndex <= aFaces.Extent(); ++anIndex)
{
const TopoDS_Face& aCandidate = TopoDS::Face(aFaces(anIndex));
const BRepAdaptor_Surface aSurface(aCandidate);
if (aSurface.GetType() == GeomAbs_Cylinder
&& std::abs(aSurface.Cylinder().Radius() - THE_ROUND_RADIUS) < Precision::Confusion())
{
aModel.myRound = aCandidate;
break;
}
}
return aModel;
}
} // namespace

// The extension of the adjacent faces used to be sized by the bounding box of
// the feature alone, which is far too short for the faces of a wedge this
// sharp to reach each other. Trimming of the extended faces then degenerated,
// the untrimmed extensions leaked into the reconstruction and no solid could
// be built.
TEST(BRepAlgoAPI_DefeaturingTest, WedgeRound_ExtensionShorterThanTheGap_RoundRemoved)
{
const WedgeModel aModel = MakeRoundedWedge();
ASSERT_FALSE(aModel.myRounded.IsNull()) << "failed to round the wedge";
ASSERT_FALSE(aModel.myRound.IsNull()) << "the round was not found";

BRepAlgoAPI_Defeaturing aDefeaturing;
aDefeaturing.SetShape(aModel.myRounded);
aDefeaturing.AddFaceToRemove(aModel.myRound);
aDefeaturing.Build();

ASSERT_TRUE(aDefeaturing.IsDone());
EXPECT_FALSE(aDefeaturing.HasWarnings()) << "the round was not removed";

const TopoDS_Shape aResult = aDefeaturing.Shape();
EXPECT_TRUE(BRepCheck_Analyzer(aResult).IsValid());

// Removing the round must give back the sharp wedge it was built on.
EXPECT_NEAR(BOPTest_Utilities::GetVolume(aModel.myWedge),
BOPTest_Utilities::GetVolume(aResult),
1.0e-3);
EXPECT_EQ(CountFaces(aModel.myWedge), CountFaces(aResult));
}
1 change: 1 addition & 0 deletions src/ModelingAlgorithms/TKBO/GTests/FILES.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ set(OCCT_TKBO_GTests_FILES
BRepAlgoAPI_Cut_Test_1.cxx
BRepAlgoAPI_Fuse_Test.cxx
BRepAlgoAPI_Common_Test.cxx
BRepAlgoAPI_Defeaturing_Test.cxx
BOPAlgo_BOP_Test.cxx
BOPAlgo_MakePeriodic_Test.cxx
BOPAlgo_PaveFiller_Test.cxx
Expand Down
Loading