Loading files
f2929057eThis file is pinned to the selected release. Download the original to open it in your design tool.
// 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 <BRep_Builder.hxx>
#include <BRep_Tool.hxx>
#include <BRepCheck_Analyzer.hxx>
#include <BRepGraphInc_Definition.hxx>
#include <BRepGraph_LayerRegistry.hxx>
#include <BRepGraphInc_Reference.hxx>
#include <BRepGraphInc_RepId.hxx>
#include <BRepBuilderAPI_Copy.hxx>
#include <BRepBuilderAPI_MakeEdge.hxx>
#include <BRepBuilderAPI_MakeFace.hxx>
#include <BRepGraph.hxx>
#include <BRepGraph_EditorView.hxx>
#include <BRepGraph_Iterator.hxx>
#include <BRepGraph_TopoView.hxx>
#include <BRepGraph_Tool.hxx>
#include <BRepGraph_NodeId.hxx>
#include <BRepGraph_ShapesView.hxx>
#include <BRepGraph_Compact.hxx>
#include <BRepGraph_Deduplicate.hxx>
#include <BRepGraph_RevisionHash.hxx>
#include <BRepGraph_UIDsView.hxx>
#include <BRepGraph_RefsView.hxx>
#include <BRepGraph_RefsIterator.hxx>
#include <BRepGraph_Validate.hxx>
#include "BRepGraph_RefTestTools.hxx"
#include <BRepTools.hxx>
#include <BRepPrimAPI_MakeBox.hxx>
#include <BRepPrimAPI_MakeCone.hxx>
#include <BRepPrimAPI_MakeCylinder.hxx>
#include <BRepPrimAPI_MakeSphere.hxx>
#include <Geom_Line.hxx>
#include <NCollection_FlatMap.hxx>
#include <NCollection_DataMap.hxx>
#include <NCollection_Map.hxx>
#include <Precision.hxx>
#include <TopAbs_ShapeEnum.hxx>
#include <TopExp_Explorer.hxx>
#include <TopoDS.hxx>
#include <TopoDS_Compound.hxx>
#include <tuple>
#include <cstdint>
#include <chrono>
#include <gtest/gtest.h>
namespace
{
//=================================================================================================
TopoDS_Compound makeTwoCopiedIdenticalFaces()
{
BRepPrimAPI_MakeBox aBoxMaker(10.0, 20.0, 30.0);
const TopoDS_Shape& aBox = aBoxMaker.Shape();
TopExp_Explorer anExp(aBox, TopAbs_FACE);
const TopoDS_Shape aFace = anExp.Current();
BRepBuilderAPI_Copy aCopy1(aFace, true);
BRepBuilderAPI_Copy aCopy2(aFace, true);
BRep_Builder aBuilder;
TopoDS_Compound aCompound;
aBuilder.MakeCompound(aCompound);
aBuilder.Add(aCompound, aCopy1.Shape());
aBuilder.Add(aCompound, aCopy2.Shape());
return aCompound;
}
//=================================================================================================
int nbUniqueFaceSurfaceDefs(const BRepGraph& theGraph)
{
NCollection_Map<const Geom_Surface*> aSurfSet;
for (BRepGraph_FullFaceIterator aFaceIt(theGraph); aFaceIt.More(); aFaceIt.Next())
{
const BRepGraph_FaceId aFaceId = aFaceIt.CurrentId();
const occ::handle<Geom_Surface>& aSurf = BRepGraph_Tool::Face::Surface(theGraph, aFaceId);
if (!aSurf.IsNull())
{
aSurfSet.Add(aSurf.get());
}
}
return aSurfSet.Extent();
}
//=================================================================================================
int nbUniqueEdgeCurveDefs(const BRepGraph& theGraph)
{
NCollection_Map<const Geom_Curve*> aCurveSet;
for (BRepGraph_FullEdgeIterator anEdgeIt(theGraph); anEdgeIt.More(); anEdgeIt.Next())
{
const BRepGraph_EdgeId anEdgeId = anEdgeIt.CurrentId();
const occ::handle<Geom_Curve>& aCurve = BRepGraph_Tool::Edge::Curve(theGraph, anEdgeId);
if (!aCurve.IsNull())
{
aCurveSet.Add(aCurve.get());
}
}
return aCurveSet.Extent();
}
//=================================================================================================
size_t nbPCurveEntries(const BRepGraph& theGraph)
{
size_t aCount = 0;
for (BRepGraph_EdgeIterator anEdgeIt(theGraph); anEdgeIt.More(); anEdgeIt.Next())
{
const BRepGraph_EdgeId anEdgeId = anEdgeIt.CurrentId();
const NCollection_LinearVector<BRepGraph_CoEdgeId>& aCoEdgeIdxs =
theGraph.Topo().Edges().CoEdges(anEdgeId);
for (size_t i = 0; i < aCoEdgeIdxs.Size(); ++i)
{
const BRepGraphInc::CoEdgeDef& aCE =
theGraph.Topo().CoEdges().Definition(aCoEdgeIdxs.Value(i));
if (aCE.FaceId.IsValid())
{
++aCount;
}
}
}
return aCount;
}
//=================================================================================================
TopoDS_Compound makeNCopiedIdenticalFaces(const int theCount)
{
BRepPrimAPI_MakeBox aBoxMaker(10.0, 20.0, 30.0);
const TopoDS_Shape& aBox = aBoxMaker.Shape();
TopExp_Explorer anExp(aBox, TopAbs_FACE);
const TopoDS_Shape aFace = anExp.Current();
BRep_Builder aBuilder;
TopoDS_Compound aCompound;
aBuilder.MakeCompound(aCompound);
for (int anIdx = 0; anIdx < theCount; ++anIdx)
{
BRepBuilderAPI_Copy aCopy(aFace, true);
aBuilder.Add(aCompound, aCopy.Shape());
}
return aCompound;
}
TopoDS_Compound makeMixedCompound()
{
BRepPrimAPI_MakeBox aBoxMaker(10.0, 20.0, 30.0);
const TopoDS_Shape& aBox = aBoxMaker.Shape();
TopExp_Explorer anExp(aBox, TopAbs_FACE);
const TopoDS_Shape aBoxFace = anExp.Current();
BRepPrimAPI_MakeCylinder aCylMaker(5.0, 15.0);
const TopoDS_Shape& aCyl = aCylMaker.Shape();
TopExp_Explorer aCylExp(aCyl, TopAbs_FACE);
const TopoDS_Shape aCylFace = aCylExp.Current();
BRep_Builder aBuilder;
TopoDS_Compound aCompound;
aBuilder.MakeCompound(aCompound);
BRepBuilderAPI_Copy aCopy1(aBoxFace, true);
BRepBuilderAPI_Copy aCopy2(aBoxFace, true);
aBuilder.Add(aCompound, aCopy1.Shape());
aBuilder.Add(aCompound, aCopy2.Shape());
aBuilder.Add(aCompound, aCylFace);
return aCompound;
}
TopoDS_Compound makeNestedCompound()
{
BRepPrimAPI_MakeBox aBoxMaker(10.0, 20.0, 30.0);
const TopoDS_Shape& aBox = aBoxMaker.Shape();
TopExp_Explorer anExp(aBox, TopAbs_FACE);
const TopoDS_Shape aFace = anExp.Current();
BRep_Builder aBuilder;
// Inner compound with 2 copies.
TopoDS_Compound anInner;
aBuilder.MakeCompound(anInner);
BRepBuilderAPI_Copy aCopy1(aFace, true);
BRepBuilderAPI_Copy aCopy2(aFace, true);
aBuilder.Add(anInner, aCopy1.Shape());
aBuilder.Add(anInner, aCopy2.Shape());
// Outer compound wrapping inner + one more copy.
TopoDS_Compound anOuter;
aBuilder.MakeCompound(anOuter);
aBuilder.Add(anOuter, anInner);
BRepBuilderAPI_Copy aCopy3(aFace, true);
aBuilder.Add(anOuter, aCopy3.Shape());
return anOuter;
}
TopoDS_Compound makeThreeDistinctPrimitives()
{
BRepPrimAPI_MakeBox aBoxMaker(10.0, 20.0, 30.0);
BRepPrimAPI_MakeSphere aSphereMaker(5.0);
BRepPrimAPI_MakeCone aConeMaker(3.0, 1.0, 8.0);
BRep_Builder aBuilder;
TopoDS_Compound aCompound;
aBuilder.MakeCompound(aCompound);
aBuilder.Add(aCompound, aBoxMaker.Shape());
aBuilder.Add(aCompound, aSphereMaker.Shape());
aBuilder.Add(aCompound, aConeMaker.Shape());
return aCompound;
}
TopoDS_Compound makeTwoIdenticalBoxes()
{
BRepPrimAPI_MakeBox aBoxMaker1(10.0, 20.0, 30.0);
BRepPrimAPI_MakeBox aBoxMaker2(10.0, 20.0, 30.0);
BRep_Builder aBuilder;
TopoDS_Compound aCompound;
aBuilder.MakeCompound(aCompound);
aBuilder.Add(aCompound, aBoxMaker1.Shape());
aBuilder.Add(aCompound, aBoxMaker2.Shape());
return aCompound;
}
uint32_t nbUniquePCurveNodes(const BRepGraph& theGraph)
{
uint32_t aCount = 0;
for (BRepGraph_FullEdgeIterator anEdgeIt(theGraph); anEdgeIt.More(); anEdgeIt.Next())
{
const BRepGraph_EdgeId anEdgeId = anEdgeIt.CurrentId();
const NCollection_LinearVector<BRepGraph_CoEdgeId>& aCoEdgeIdxs =
theGraph.Topo().Edges().CoEdges(anEdgeId);
aCount += static_cast<uint32_t>(aCoEdgeIdxs.Size());
}
return aCount;
}
int addDuplicatePCurvesToAllEdges(BRepGraph& theGraph)
{
int aDupCount = 0;
for (BRepGraph_FullEdgeIterator anEdgeIt(theGraph); anEdgeIt.More(); anEdgeIt.Next())
{
const BRepGraph_EdgeId anEdgeId = anEdgeIt.CurrentId();
const NCollection_LinearVector<BRepGraph_CoEdgeId>& aCoEdgeIdxs =
theGraph.Topo().Edges().CoEdges(anEdgeId);
if (aCoEdgeIdxs.IsEmpty())
{
continue;
}
const BRepGraph_CoEdgeId aCoEdgeId = aCoEdgeIdxs.Value(0);
const BRepGraphInc::CoEdgeDef& aCE = theGraph.Topo().CoEdges().Definition(aCoEdgeId);
if (!aCE.Curve2DRepId.IsValid())
{
continue;
}
const std::pair<double, double> aRange = BRepGraph_Tool::CoEdge::Range(theGraph, aCoEdgeId);
const occ::handle<Geom2d_Curve>& aDupPCurve =
BRepGraph_Tool::CoEdge::PCurve(theGraph, aCoEdgeId);
std::ignore = theGraph.Editor().CoEdges().Add(anEdgeId,
aCE.FaceId,
aDupPCurve,
aRange.first,
aRange.second,
aCE.Orientation);
++aDupCount;
}
return aDupCount;
}
//=================================================================================================
TopoDS_Compound makeThreeCopiedIdenticalEdges()
{
BRepPrimAPI_MakeBox aBoxMaker(10.0, 20.0, 30.0);
const TopoDS_Shape& aBox = aBoxMaker.Shape();
TopExp_Explorer anExp(aBox, TopAbs_EDGE);
const TopoDS_Shape aEdge = anExp.Current();
BRepBuilderAPI_Copy aCopy1(aEdge, true);
BRepBuilderAPI_Copy aCopy2(aEdge, true);
BRep_Builder aBuilder;
TopoDS_Compound aCompound;
aBuilder.MakeCompound(aCompound);
aBuilder.Add(aCompound, aEdge);
aBuilder.Add(aCompound, aCopy1.Shape());
aBuilder.Add(aCompound, aCopy2.Shape());
return aCompound;
}
void addTwoReversedEdgesOnSharedCurve(BRepGraph& theGraph)
{
const gp_Pnt aStart(0.0, 0.0, 0.0);
const gp_Pnt anEnd(10.0, 0.0, 0.0);
const BRepGraph_VertexId aStartVertex = theGraph.Editor().Vertices().Add(aStart, 1.0e-7);
const BRepGraph_VertexId anEndVertex = theGraph.Editor().Vertices().Add(anEnd, 1.0e-7);
const occ::handle<Geom_Curve> aCurve = new Geom_Line(aStart, gp_Dir(1.0, 0.0, 0.0));
std::ignore = theGraph.Editor().Edges().Add(aStartVertex, anEndVertex, aCurve, 0.0, 10.0, 1.0e-7);
std::ignore = theGraph.Editor().Edges().Add(anEndVertex, aStartVertex, aCurve, 0.0, 10.0, 1.0e-7);
}
TopoDS_Compound makeTwoCopiedFacesWithInnerWire()
{
gp_Pln aPlane(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(0.0, 0.0, 1.0));
BRep_Builder aBuilder;
TopoDS_Wire anOuterWire;
aBuilder.MakeWire(anOuterWire);
aBuilder.Add(anOuterWire, BRepBuilderAPI_MakeEdge(gp_Pnt(0.0, 0.0, 0.0), gp_Pnt(10.0, 0.0, 0.0)));
aBuilder.Add(anOuterWire,
BRepBuilderAPI_MakeEdge(gp_Pnt(10.0, 0.0, 0.0), gp_Pnt(10.0, 10.0, 0.0)));
aBuilder.Add(anOuterWire,
BRepBuilderAPI_MakeEdge(gp_Pnt(10.0, 10.0, 0.0), gp_Pnt(0.0, 10.0, 0.0)));
aBuilder.Add(anOuterWire, BRepBuilderAPI_MakeEdge(gp_Pnt(0.0, 10.0, 0.0), gp_Pnt(0.0, 0.0, 0.0)));
TopoDS_Wire anInnerWire;
aBuilder.MakeWire(anInnerWire);
aBuilder.Add(anInnerWire, BRepBuilderAPI_MakeEdge(gp_Pnt(3.0, 3.0, 0.0), gp_Pnt(3.0, 7.0, 0.0)));
aBuilder.Add(anInnerWire, BRepBuilderAPI_MakeEdge(gp_Pnt(3.0, 7.0, 0.0), gp_Pnt(7.0, 7.0, 0.0)));
aBuilder.Add(anInnerWire, BRepBuilderAPI_MakeEdge(gp_Pnt(7.0, 7.0, 0.0), gp_Pnt(7.0, 3.0, 0.0)));
aBuilder.Add(anInnerWire, BRepBuilderAPI_MakeEdge(gp_Pnt(7.0, 3.0, 0.0), gp_Pnt(3.0, 3.0, 0.0)));
BRepBuilderAPI_MakeFace aFaceMaker(aPlane, anOuterWire);
aFaceMaker.Add(anInnerWire);
const TopoDS_Face aFace = aFaceMaker.Face();
BRepBuilderAPI_Copy aCopy1(aFace, true);
BRepBuilderAPI_Copy aCopy2(aFace, true);
TopoDS_Compound aCompound;
aBuilder.MakeCompound(aCompound);
aBuilder.Add(aCompound, aCopy1.Shape());
aBuilder.Add(aCompound, aCopy2.Shape());
return aCompound;
}
//=================================================================================================
TopoDS_Compound makeTwoFaceTouchingBoxes()
{
BRepPrimAPI_MakeBox aBoxMaker1(gp_Pnt(0, 0, 0), 10.0, 10.0, 10.0);
BRepPrimAPI_MakeBox aBoxMaker2(gp_Pnt(0, 0, 10), 10.0, 10.0, 10.0);
BRep_Builder aBuilder;
TopoDS_Compound aCompound;
aBuilder.MakeCompound(aCompound);
aBuilder.Add(aCompound, aBoxMaker1.Shape());
aBuilder.Add(aCompound, aBoxMaker2.Shape());
return aCompound;
}
} // namespace
TEST(BRepGraph_DeduplicateTest, AnalyzeOnly_DoesNotRewrite)
{
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes1 =
aGraph.Shapes().Add(makeTwoCopiedIdenticalFaces());
ASSERT_FALSE(aGraph.IsEmpty());
ASSERT_EQ(nbUniqueFaceSurfaceDefs(aGraph), 2);
BRepGraph_Deduplicate::Options anOpts;
anOpts.AnalyzeOnly = true;
const BRepGraph_Deduplicate::Result aRes = BRepGraph_Deduplicate::Perform(aGraph, anOpts);
EXPECT_EQ(aRes.NbSurfaceRewrites, 0);
EXPECT_EQ(nbUniqueFaceSurfaceDefs(aGraph), 2);
}
TEST(BRepGraph_DeduplicateTest, AnalyzeOnly_ReportsCanonicalCandidates)
{
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes2 =
aGraph.Shapes().Add(makeTwoCopiedIdenticalFaces());
ASSERT_FALSE(aGraph.IsEmpty());
BRepGraph_Deduplicate::Options anOpts;
anOpts.AnalyzeOnly = true;
const BRepGraph_Deduplicate::Result aRes = BRepGraph_Deduplicate::Perform(aGraph, anOpts);
// TwoCopiedFaces: 2 surfaces -> 1 canonical, 8 curves -> 4 canonical, 8 PCurves -> 8 canonical.
EXPECT_EQ(aRes.NbCanonicalSurfaces, 1);
EXPECT_EQ(aRes.NbCanonicalCurves, 4);
EXPECT_EQ(aRes.NbSurfaceRewrites, 0);
EXPECT_EQ(aRes.NbCurveRewrites, 0);
}
TEST(BRepGraph_DeduplicateTest, CanonicalizeSurfaces_Rewrites)
{
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes3 =
aGraph.Shapes().Add(makeTwoCopiedIdenticalFaces());
ASSERT_FALSE(aGraph.IsEmpty());
ASSERT_EQ(nbUniqueFaceSurfaceDefs(aGraph), 2);
BRepGraph_Deduplicate::Options anOpts;
anOpts.AnalyzeOnly = false;
const BRepGraph_Deduplicate::Result aRes = BRepGraph_Deduplicate::Perform(aGraph, anOpts);
EXPECT_EQ(aRes.NbSurfaceRewrites, 1);
EXPECT_EQ(nbUniqueFaceSurfaceDefs(aGraph), 1);
}
TEST(BRepGraph_DeduplicateTest, CanonicalizeCurves_RewritesAndReducesUnique)
{
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes4 =
aGraph.Shapes().Add(makeTwoCopiedIdenticalFaces());
ASSERT_FALSE(aGraph.IsEmpty());
ASSERT_EQ(nbUniqueEdgeCurveDefs(aGraph), 8);
BRepGraph_Deduplicate::Options anOpts;
anOpts.AnalyzeOnly = false;
const BRepGraph_Deduplicate::Result aRes = BRepGraph_Deduplicate::Perform(aGraph, anOpts);
EXPECT_EQ(aRes.NbCurveRewrites, 4);
EXPECT_EQ(nbUniqueEdgeCurveDefs(aGraph), 4);
}
TEST(BRepGraph_DeduplicateTest, DefaultOverload_PerformWorks)
{
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes7 =
aGraph.Shapes().Add(makeTwoCopiedIdenticalFaces());
ASSERT_FALSE(aGraph.IsEmpty());
const BRepGraph_Deduplicate::Result aRes = BRepGraph_Deduplicate::Perform(aGraph);
EXPECT_EQ(aRes.NbCanonicalSurfaces, 1);
EXPECT_EQ(aRes.NbSurfaceRewrites, 1);
EXPECT_EQ(aRes.NbCurveRewrites, 4);
EXPECT_FALSE(aRes.IsEntityMergeApplied);
}
TEST(BRepGraph_DeduplicateTest, SingleFace_NoSurfaceRewrite)
{
BRepPrimAPI_MakeBox aBoxMaker(10.0, 20.0, 30.0);
const TopoDS_Shape& aBox = aBoxMaker.Shape();
TopExp_Explorer anExp(aBox, TopAbs_FACE);
ASSERT_TRUE(anExp.More());
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes8 =
aGraph.Shapes().Add(anExp.Current());
ASSERT_FALSE(aGraph.IsEmpty());
const BRepGraph_Deduplicate::Result aRes = BRepGraph_Deduplicate::Perform(aGraph);
EXPECT_EQ(aRes.NbSurfaceRewrites, 0);
}
// PCurve dedup test removed - PCurves are now inline on EdgeDef, not separate graph nodes.
TEST(BRepGraph_DeduplicateTest, NotDoneGraph_ReturnsEmptyResult)
{
BRepGraph aGraph;
// Do not call BRepGraph::ShapesView::Add() - graph is not done.
ASSERT_TRUE(aGraph.IsEmpty());
const BRepGraph_Deduplicate::Result aRes = BRepGraph_Deduplicate::Perform(aGraph);
EXPECT_EQ(aRes.NbCanonicalSurfaces, 0);
EXPECT_EQ(aRes.NbCanonicalCurves, 0);
EXPECT_EQ(aRes.NbSurfaceRewrites, 0);
EXPECT_EQ(aRes.NbCurveRewrites, 0);
EXPECT_EQ(aRes.NbNullifiedSurfaces, 0);
EXPECT_EQ(aRes.NbNullifiedCurves, 0);
EXPECT_FALSE(aRes.IsEntityMergeApplied);
EXPECT_EQ(aRes.NbReorderedWires, 0);
EXPECT_EQ(aRes.NbToleranceOrderedWires, 0);
EXPECT_EQ(aRes.NbPartialOrderedWires, 0);
}
TEST(BRepGraph_DeduplicateTest, Idempotent_SecondRunNoRewrites)
{
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes9 =
aGraph.Shapes().Add(makeTwoCopiedIdenticalFaces());
ASSERT_FALSE(aGraph.IsEmpty());
const BRepGraph_Deduplicate::Result aRes1 = BRepGraph_Deduplicate::Perform(aGraph);
ASSERT_EQ(aRes1.NbSurfaceRewrites, 1);
ASSERT_EQ(aRes1.NbCurveRewrites, 4);
// After first dedup, all faces share the same surface pointer, all duplicate
// edges share the same curve pointer. A second run still detects the same
// canonical mapping via GeomHash, so rewrite counts may be non-zero (no-op
// at pointer level). Verify the unique pointer count is unchanged.
const int aUniqueSurfsBefore = nbUniqueFaceSurfaceDefs(aGraph);
const int aUniqueCurvesBefore = nbUniqueEdgeCurveDefs(aGraph);
const BRepGraph_Deduplicate::Result aRes2 = BRepGraph_Deduplicate::Perform(aGraph);
EXPECT_EQ(nbUniqueFaceSurfaceDefs(aGraph), aUniqueSurfsBefore);
EXPECT_EQ(nbUniqueEdgeCurveDefs(aGraph), aUniqueCurvesBefore);
}
TEST(BRepGraph_DeduplicateTest, FullBox_AllSurfacesUnique)
{
BRepPrimAPI_MakeBox aBoxMaker(10.0, 20.0, 30.0);
const TopoDS_Shape& aBox = aBoxMaker.Shape();
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes10 = aGraph.Shapes().Add(aBox);
ASSERT_FALSE(aGraph.IsEmpty());
// A box has 6 faces with 6 distinct Geom_Plane instances (different origins/normals).
// No surface dedup should occur - all 6 are canonical.
const BRepGraph_Deduplicate::Result aRes = BRepGraph_Deduplicate::Perform(aGraph);
EXPECT_EQ(aRes.NbCanonicalSurfaces, 6);
EXPECT_EQ(aRes.NbSurfaceRewrites, 0);
}
TEST(BRepGraph_DeduplicateTest, ResultCountersConsistency)
{
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes11 =
aGraph.Shapes().Add(makeTwoCopiedIdenticalFaces());
ASSERT_FALSE(aGraph.IsEmpty());
// TwoCopiedFaces: 2 surfaces, 8 curves, 8 PCurves.
ASSERT_EQ(aGraph.Topo().Faces().Nb(), 2);
ASSERT_EQ(aGraph.Topo().Edges().Nb(), 8);
ASSERT_EQ(nbPCurveEntries(aGraph), 8);
BRepGraph_Deduplicate::Options anOpts;
anOpts.AnalyzeOnly = true;
const BRepGraph_Deduplicate::Result aRes = BRepGraph_Deduplicate::Perform(aGraph, anOpts);
// Canonical + duplicates == total for each geometry type.
EXPECT_EQ(aRes.NbCanonicalSurfaces, 1);
EXPECT_EQ(aRes.NbCanonicalSurfaces + (aGraph.Topo().Faces().Nb() - aRes.NbCanonicalSurfaces),
aGraph.Topo().Faces().Nb());
EXPECT_EQ(aRes.NbCanonicalCurves, 4);
EXPECT_EQ(aRes.NbCanonicalCurves + (aGraph.Topo().Edges().Nb() - aRes.NbCanonicalCurves),
aGraph.Topo().Edges().Nb());
}
TEST(BRepGraph_DeduplicateTest, EmptyCompound_NoRewrites)
{
BRep_Builder aBuilder;
TopoDS_Compound aCompound;
aBuilder.MakeCompound(aCompound);
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes12 = aGraph.Shapes().Add(aCompound);
ASSERT_FALSE(aGraph.IsEmpty());
const BRepGraph_Deduplicate::Result aRes = BRepGraph_Deduplicate::Perform(aGraph);
EXPECT_EQ(aRes.NbSurfaceRewrites, 0);
EXPECT_EQ(aRes.NbCurveRewrites, 0);
}
TEST(BRepGraph_DeduplicateTest, MultipleCopies_NWayDedup)
{
const int aNbCopies = 4;
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes13 =
aGraph.Shapes().Add(makeNCopiedIdenticalFaces(aNbCopies));
ASSERT_FALSE(aGraph.IsEmpty());
ASSERT_EQ(aGraph.Topo().Faces().Nb(), aNbCopies);
const BRepGraph_Deduplicate::Result aRes = BRepGraph_Deduplicate::Perform(aGraph);
EXPECT_EQ(aRes.NbCanonicalSurfaces, 1);
EXPECT_EQ(aRes.NbSurfaceRewrites, aNbCopies - 1);
EXPECT_EQ(nbUniqueFaceSurfaceDefs(aGraph), 1);
}
TEST(BRepGraph_DeduplicateTest, MixedGeometry_OnlyIdenticalDeduped)
{
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes14 =
aGraph.Shapes().Add(makeMixedCompound());
ASSERT_FALSE(aGraph.IsEmpty());
// 2 box face copies + 1 cylinder face = 3 faces, 3 surfaces, 11 curves.
ASSERT_EQ(aGraph.Topo().Faces().Nb(), 3);
ASSERT_EQ(aGraph.Topo().Faces().Nb(), 3);
ASSERT_EQ(aGraph.Topo().Edges().Nb(), 11);
const BRepGraph_Deduplicate::Result aRes = BRepGraph_Deduplicate::Perform(aGraph);
// Only the two identical box faces should be deduped; cylinder face is untouched.
EXPECT_EQ(aRes.NbCanonicalSurfaces, 2);
EXPECT_EQ(aRes.NbCanonicalCurves, 7);
EXPECT_EQ(aRes.NbSurfaceRewrites, 1);
EXPECT_EQ(aRes.NbCurveRewrites, 4);
// 7 canonicalize + 1 nullify surface + 4 nullify curves + 2 nullify PCurves = 14.
EXPECT_EQ(nbUniqueFaceSurfaceDefs(aGraph), 2);
EXPECT_EQ(nbUniqueEdgeCurveDefs(aGraph), 7);
}
TEST(BRepGraph_DeduplicateTest, AfterDedup_AllCopiedFacesShareCanonicalSurface)
{
const int aNbCopies = 3;
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes15 =
aGraph.Shapes().Add(makeNCopiedIdenticalFaces(aNbCopies));
ASSERT_FALSE(aGraph.IsEmpty());
std::ignore = BRepGraph_Deduplicate::Perform(aGraph);
// All face defs should share the same Surface handle.
NCollection_Map<const Geom_Surface*> aSurfIds;
for (BRepGraph_FullFaceIterator aFaceIt(aGraph); aFaceIt.More(); aFaceIt.Next())
{
const BRepGraph_FaceId aFaceId = aFaceIt.CurrentId();
const occ::handle<Geom_Surface>& aSurf = BRepGraph_Tool::Face::Surface(aGraph, aFaceId);
if (!aSurf.IsNull())
{
aSurfIds.Add(aSurf.get());
}
}
EXPECT_EQ(aSurfIds.Extent(), 1);
}
TEST(BRepGraph_DeduplicateTest, AnalyzeOnly_CurveAndPCurveCountsReported)
{
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes17 =
aGraph.Shapes().Add(makeTwoCopiedIdenticalFaces());
ASSERT_FALSE(aGraph.IsEmpty());
BRepGraph_Deduplicate::Options anOpts;
anOpts.AnalyzeOnly = true;
const BRepGraph_Deduplicate::Result aRes = BRepGraph_Deduplicate::Perform(aGraph, anOpts);
// TwoCopiedFaces: 2 geom surfaces, 8 curves, 8 PCurves.
EXPECT_EQ(aRes.NbCanonicalSurfaces, 1);
EXPECT_EQ(aRes.NbCanonicalCurves, 4);
// But no rewrites.
EXPECT_EQ(aRes.NbSurfaceRewrites, 0);
EXPECT_EQ(aRes.NbCurveRewrites, 0);
}
// ---------------------------------------------------------------------------
// Tolerance and Options tests
// ---------------------------------------------------------------------------
TEST(BRepGraph_DeduplicateTest, DefaultOptionsStruct_HasExpectedDefaults)
{
BRepGraph_Deduplicate::Options anOpts;
EXPECT_FALSE(anOpts.AnalyzeOnly);
EXPECT_FALSE(anOpts.MergeEntitiesWhenSafe);
EXPECT_NEAR(anOpts.CompTolerance, Precision::Angular(), 1e-20);
EXPECT_NEAR(anOpts.HashTolerance, Precision::Confusion(), 1e-20);
}
TEST(BRepGraph_DeduplicateTest, DefaultResultStruct_AllZeroed)
{
BRepGraph_Deduplicate::Result aRes;
EXPECT_EQ(aRes.NbCanonicalSurfaces, 0);
EXPECT_EQ(aRes.NbCanonicalCurves, 0);
EXPECT_EQ(aRes.NbSurfaceRewrites, 0);
EXPECT_EQ(aRes.NbCurveRewrites, 0);
EXPECT_EQ(aRes.NbNullifiedSurfaces, 0);
EXPECT_EQ(aRes.NbNullifiedCurves, 0);
EXPECT_FALSE(aRes.IsEntityMergeApplied);
EXPECT_EQ(aRes.NbReorderedWires, 0);
EXPECT_EQ(aRes.NbToleranceOrderedWires, 0);
EXPECT_EQ(aRes.NbPartialOrderedWires, 0);
}
TEST(BRepGraph_DeduplicateTest, MergeDefsWhenSafe_MergesVertices)
{
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes18 =
aGraph.Shapes().Add(makeTwoCopiedIdenticalFaces());
ASSERT_FALSE(aGraph.IsEmpty());
const uint32_t aNbVerticesBefore = aGraph.Topo().Vertices().Nb();
BRepGraph_Deduplicate::Options anOpts;
anOpts.MergeEntitiesWhenSafe = true;
const BRepGraph_Deduplicate::Result aRes = BRepGraph_Deduplicate::Perform(aGraph, anOpts);
// Two copied faces share identical vertex positions -> vertices should merge.
EXPECT_GT(aRes.NbMergedVertices, 0);
EXPECT_TRUE(aRes.IsEntityMergeApplied);
std::ignore = aNbVerticesBefore;
}
// ---------------------------------------------------------------------------
// Nested and complex compound tests
// ---------------------------------------------------------------------------
TEST(BRepGraph_DeduplicateTest, NestedCompound_AllCopiesDeduped)
{
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes19 =
aGraph.Shapes().Add(makeNestedCompound());
ASSERT_FALSE(aGraph.IsEmpty());
// 3 copies of the same face across nested compounds: 3 surfaces, 12 curves.
ASSERT_EQ(aGraph.Topo().Faces().Nb(), 3);
ASSERT_EQ(aGraph.Topo().Edges().Nb(), 12);
ASSERT_EQ(aGraph.Topo().Faces().Nb(), 3);
ASSERT_EQ(aGraph.Topo().Edges().Nb(), 12);
const BRepGraph_Deduplicate::Result aRes = BRepGraph_Deduplicate::Perform(aGraph);
EXPECT_EQ(aRes.NbCanonicalSurfaces, 1);
EXPECT_EQ(aRes.NbCanonicalCurves, 4);
EXPECT_EQ(aRes.NbSurfaceRewrites, 2);
EXPECT_EQ(aRes.NbCurveRewrites, 8);
// 10 canonicalize + 2 nullify surfaces + 8 nullify curves = 20.
EXPECT_EQ(nbUniqueFaceSurfaceDefs(aGraph), 1);
EXPECT_EQ(nbUniqueEdgeCurveDefs(aGraph), 4);
}
TEST(BRepGraph_DeduplicateTest, ThreeDistinctPrimitives_MinimalDedup)
{
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes20 =
aGraph.Shapes().Add(makeThreeDistinctPrimitives());
ASSERT_FALSE(aGraph.IsEmpty());
// Box(6 faces) + Sphere(1 face) + Cone(3 faces) = 10 faces, 18 edges.
ASSERT_EQ(aGraph.Topo().Faces().Nb(), 10);
ASSERT_EQ(aGraph.Topo().Edges().Nb(), 18);
const BRepGraph_Deduplicate::Result aRes = BRepGraph_Deduplicate::Perform(aGraph);
// 9 canonical surfaces (1 surface matches between cone bottom and sphere/box plane).
EXPECT_EQ(aRes.NbCanonicalSurfaces, 9);
EXPECT_EQ(aRes.NbSurfaceRewrites, 1);
EXPECT_EQ(nbUniqueFaceSurfaceDefs(aGraph), 9);
// Some curve dedup may occur between the three primitives (shared circle/line geometry).
EXPECT_LE(nbUniqueEdgeCurveDefs(aGraph), 18);
}
TEST(BRepGraph_DeduplicateTest, TwoIdenticalBoxes_SurfacesAndCurvesDeduped)
{
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes21 =
aGraph.Shapes().Add(makeTwoIdenticalBoxes());
ASSERT_FALSE(aGraph.IsEmpty());
// Two identical boxes: 12 faces, 24 edges, 12 geom surfaces, 24 geom curves.
ASSERT_EQ(aGraph.Topo().Faces().Nb(), 12);
ASSERT_EQ(aGraph.Topo().Edges().Nb(), 24);
ASSERT_EQ(aGraph.Topo().Faces().Nb(), 12);
ASSERT_EQ(aGraph.Topo().Edges().Nb(), 24);
const BRepGraph_Deduplicate::Result aRes = BRepGraph_Deduplicate::Perform(aGraph);
EXPECT_EQ(aRes.NbCanonicalSurfaces, 6);
EXPECT_EQ(aRes.NbCanonicalCurves, 12);
EXPECT_EQ(aRes.NbSurfaceRewrites, 6);
EXPECT_EQ(aRes.NbCurveRewrites, 12);
// 18 canonicalize + 6 nullify surfaces + 12 nullify curves = 36.
EXPECT_EQ(nbUniqueFaceSurfaceDefs(aGraph), 6);
EXPECT_EQ(nbUniqueEdgeCurveDefs(aGraph), 12);
}
// ---------------------------------------------------------------------------
// Curve-focused tests
// ---------------------------------------------------------------------------
TEST(BRepGraph_DeduplicateTest, CurveRewriteCount_MatchesDuplicateEdgeCurves)
{
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes22 =
aGraph.Shapes().Add(makeTwoCopiedIdenticalFaces());
ASSERT_FALSE(aGraph.IsEmpty());
// TwoCopiedFaces: 8 geom curves, 4 canonical -> 4 duplicates.
ASSERT_EQ(aGraph.Topo().Edges().Nb(), 8);
BRepGraph_Deduplicate::Options anAnalyze;
anAnalyze.AnalyzeOnly = true;
const BRepGraph_Deduplicate::Result anAnalysis =
BRepGraph_Deduplicate::Perform(aGraph, anAnalyze);
EXPECT_EQ(anAnalysis.NbCanonicalCurves, 4);
const BRepGraph_Deduplicate::Result aRes = BRepGraph_Deduplicate::Perform(aGraph);
// 4 duplicate curve nodes -> exactly 4 edge curve rewrites.
EXPECT_EQ(aRes.NbCurveRewrites, 4);
}
TEST(BRepGraph_DeduplicateTest, AfterDedup_AllCopiedEdgesShareCanonicalCurve)
{
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes23 =
aGraph.Shapes().Add(makeTwoCopiedIdenticalFaces());
ASSERT_FALSE(aGraph.IsEmpty());
ASSERT_EQ(nbUniqueEdgeCurveDefs(aGraph), 8);
std::ignore = BRepGraph_Deduplicate::Perform(aGraph);
// After dedup: 8 unique curves reduced to 4 canonical.
EXPECT_EQ(nbUniqueEdgeCurveDefs(aGraph), 4);
}
// ---------------------------------------------------------------------------
// PCurve tests
// ---------------------------------------------------------------------------
TEST(BRepGraph_DeduplicateTest, MultiplePCurveDups_AllEdgesDeduped)
{
BRepPrimAPI_MakeBox aBoxMaker(10.0, 20.0, 30.0);
TopExp_Explorer anExp(aBoxMaker.Shape(), TopAbs_FACE);
BRepBuilderAPI_Copy aCopy(anExp.Current(), true);
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes24 =
aGraph.Shapes().Add(aCopy.Shape());
ASSERT_FALSE(aGraph.IsEmpty());
const int aDupCount = addDuplicatePCurvesToAllEdges(aGraph);
ASSERT_GT(aDupCount, 0);
const BRepGraph_Deduplicate::Result aRes = BRepGraph_Deduplicate::Perform(aGraph);
}
TEST(BRepGraph_DeduplicateTest, PCurveDup_AnalyzeOnly_CountsButNoRewrite)
{
BRepPrimAPI_MakeBox aBoxMaker(10.0, 20.0, 30.0);
TopExp_Explorer anExp(aBoxMaker.Shape(), TopAbs_FACE);
BRepBuilderAPI_Copy aCopy(anExp.Current(), true);
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes25 =
aGraph.Shapes().Add(aCopy.Shape());
ASSERT_FALSE(aGraph.IsEmpty());
const int aDupCount = addDuplicatePCurvesToAllEdges(aGraph);
ASSERT_GT(aDupCount, 0);
const uint32_t aUniqueBefore = nbUniquePCurveNodes(aGraph);
BRepGraph_Deduplicate::Options anOpts;
anOpts.AnalyzeOnly = true;
const BRepGraph_Deduplicate::Result aRes = BRepGraph_Deduplicate::Perform(aGraph, anOpts);
EXPECT_EQ(nbUniquePCurveNodes(aGraph), aUniqueBefore);
// Canonical count should be less than total PCurves.
}
TEST(BRepGraph_DeduplicateTest, NoPCurveDuplicates_ZeroPCurveRewrites)
{
// A single face copy has no PCurve duplicates.
BRepPrimAPI_MakeBox aBoxMaker(10.0, 20.0, 30.0);
TopExp_Explorer anExp(aBoxMaker.Shape(), TopAbs_FACE);
BRepBuilderAPI_Copy aCopy(anExp.Current(), true);
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes26 =
aGraph.Shapes().Add(aCopy.Shape());
ASSERT_FALSE(aGraph.IsEmpty());
const BRepGraph_Deduplicate::Result aRes = BRepGraph_Deduplicate::Perform(aGraph);
}
// ---------------------------------------------------------------------------
// Canonical node validity tests
// ---------------------------------------------------------------------------
TEST(BRepGraph_DeduplicateTest, AfterDedup_AllSurfacesValid)
{
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes32 =
aGraph.Shapes().Add(makeTwoCopiedIdenticalFaces());
ASSERT_FALSE(aGraph.IsEmpty());
std::ignore = BRepGraph_Deduplicate::Perform(aGraph);
for (BRepGraph_FullFaceIterator aFaceIt(aGraph); aFaceIt.More(); aFaceIt.Next())
{
const BRepGraph_FaceId aFaceId = aFaceIt.CurrentId();
const BRepGraphInc::FaceDef& aFaceDef = aGraph.Topo().Faces().Definition(aFaceId);
EXPECT_TRUE(aFaceDef.SurfaceRepId.IsValid())
<< "Face " << aFaceId.Index << " has null Surface after dedup";
}
}
TEST(BRepGraph_DeduplicateTest, AfterDedup_AllCurve3dsValid)
{
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes33 =
aGraph.Shapes().Add(makeTwoCopiedIdenticalFaces());
ASSERT_FALSE(aGraph.IsEmpty());
std::ignore = BRepGraph_Deduplicate::Perform(aGraph);
for (BRepGraph_FullEdgeIterator anEdgeIt(aGraph); anEdgeIt.More(); anEdgeIt.Next())
{
const BRepGraph_EdgeId anEdgeId = anEdgeIt.CurrentId();
const BRepGraphInc::EdgeDef& anEdgeDef = aGraph.Topo().Edges().Definition(anEdgeId);
if (!BRepGraph_Tool::Edge::Degenerated(aGraph, anEdgeId))
{
EXPECT_TRUE(anEdgeDef.Curve3DRepId.IsValid())
<< "Edge " << anEdgeId.Index << " has null Curve3d after dedup";
}
}
}
TEST(BRepGraph_DeduplicateTest, AfterDedup_AllInlinePCurvesHaveCurve2d)
{
BRepPrimAPI_MakeBox aBoxMaker(10.0, 20.0, 30.0);
TopExp_Explorer anExp(aBoxMaker.Shape(), TopAbs_FACE);
BRepBuilderAPI_Copy aCopy(anExp.Current(), true);
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes34 =
aGraph.Shapes().Add(aCopy.Shape());
ASSERT_FALSE(aGraph.IsEmpty());
std::ignore = addDuplicatePCurvesToAllEdges(aGraph);
std::ignore = BRepGraph_Deduplicate::Perform(aGraph);
for (BRepGraph_FullEdgeIterator anEdgeIt(aGraph); anEdgeIt.More(); anEdgeIt.Next())
{
const BRepGraph_EdgeId anEdgeId = anEdgeIt.CurrentId();
const NCollection_LinearVector<BRepGraph_CoEdgeId>& aCoEdgeIdxs =
aGraph.Topo().Edges().CoEdges(anEdgeId);
for (size_t aCEIter = 0; aCEIter < aCoEdgeIdxs.Size(); ++aCEIter)
{
const BRepGraphInc::CoEdgeDef& aCE =
aGraph.Topo().CoEdges().Definition(aCoEdgeIdxs.Value(aCEIter));
EXPECT_TRUE(aCE.Curve2DRepId.IsValid())
<< "Edge " << anEdgeId.Index << " CoEdge " << aCEIter << " has null Curve2d after dedup";
}
}
}
TEST(BRepGraph_DeduplicateTest, AfterDedup_CanonicalSurfaceGeomNotNull)
{
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes35 =
aGraph.Shapes().Add(makeNCopiedIdenticalFaces(4));
ASSERT_FALSE(aGraph.IsEmpty());
std::ignore = BRepGraph_Deduplicate::Perform(aGraph);
// All face defs should have a non-null surface after dedup.
for (BRepGraph_FullFaceIterator aFaceIt(aGraph); aFaceIt.More(); aFaceIt.Next())
{
const BRepGraph_FaceId aFaceId = aFaceIt.CurrentId();
const BRepGraphInc::FaceDef& aFaceDef = aGraph.Topo().Faces().Definition(aFaceId);
EXPECT_TRUE(aFaceDef.SurfaceRepId.IsValid())
<< "Face " << aFaceId.Index << " has null Surface after dedup";
}
}
TEST(BRepGraph_DeduplicateTest, AfterDedup_CanonicalCurveGeomNotNull)
{
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes36 =
aGraph.Shapes().Add(makeTwoCopiedIdenticalFaces());
ASSERT_FALSE(aGraph.IsEmpty());
std::ignore = BRepGraph_Deduplicate::Perform(aGraph);
for (BRepGraph_FullEdgeIterator anEdgeIt(aGraph); anEdgeIt.More(); anEdgeIt.Next())
{
const BRepGraph_EdgeId anEdgeId = anEdgeIt.CurrentId();
const BRepGraphInc::EdgeDef& anEdgeDef = aGraph.Topo().Edges().Definition(anEdgeId);
if (!BRepGraph_Tool::Edge::Degenerated(aGraph, anEdgeId))
{
EXPECT_TRUE(anEdgeDef.Curve3DRepId.IsValid())
<< "Edge " << anEdgeId.Index << " has null Curve3d after dedup";
}
}
}
// ---------------------------------------------------------------------------
// Parallel build + dedup tests
// ---------------------------------------------------------------------------
TEST(BRepGraph_DeduplicateTest, ParallelBuild_SameResultAsSequential)
{
const TopoDS_Compound aCompound = makeTwoCopiedIdenticalFaces();
BRepGraph aGraphSeq;
aGraphSeq.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes37 =
aGraphSeq.Shapes().Add(aCompound, BRepGraph::ShapesView::Options{{}, true, false, false});
ASSERT_FALSE(aGraphSeq.IsEmpty());
BRepGraph aGraphPar;
aGraphPar.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes38 =
aGraphPar.Shapes().Add(aCompound, BRepGraph::ShapesView::Options{{}, true, false, true});
ASSERT_FALSE(aGraphPar.IsEmpty());
const BRepGraph_Deduplicate::Result aResSeq = BRepGraph_Deduplicate::Perform(aGraphSeq);
const BRepGraph_Deduplicate::Result aResPar = BRepGraph_Deduplicate::Perform(aGraphPar);
// Both should produce identical results: 1 canonical surface, 4 canonical curves.
EXPECT_EQ(aResSeq.NbCanonicalSurfaces, 1);
EXPECT_EQ(aResPar.NbCanonicalSurfaces, 1);
EXPECT_EQ(aResSeq.NbCanonicalCurves, 4);
EXPECT_EQ(aResPar.NbCanonicalCurves, 4);
EXPECT_EQ(aResSeq.NbSurfaceRewrites, 1);
EXPECT_EQ(aResPar.NbSurfaceRewrites, 1);
EXPECT_EQ(aResSeq.NbCurveRewrites, 4);
EXPECT_EQ(aResPar.NbCurveRewrites, 4);
}
// ---------------------------------------------------------------------------
// Scale / many copies tests
// ---------------------------------------------------------------------------
TEST(BRepGraph_DeduplicateTest, TenCopies_AllDeduplicatedToOneSurface)
{
const int aNbCopies = 10;
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes39 =
aGraph.Shapes().Add(makeNCopiedIdenticalFaces(aNbCopies));
ASSERT_FALSE(aGraph.IsEmpty());
ASSERT_EQ(aGraph.Topo().Faces().Nb(), aNbCopies);
const BRepGraph_Deduplicate::Result aRes = BRepGraph_Deduplicate::Perform(aGraph);
EXPECT_EQ(aRes.NbCanonicalSurfaces, 1);
EXPECT_EQ(aRes.NbSurfaceRewrites, aNbCopies - 1);
EXPECT_EQ(nbUniqueFaceSurfaceDefs(aGraph), 1);
}
TEST(BRepGraph_DeduplicateTest, TwoCopies_CurveCanonicalCountLessThanTotal)
{
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes40 =
aGraph.Shapes().Add(makeTwoCopiedIdenticalFaces());
ASSERT_FALSE(aGraph.IsEmpty());
// 8 geom curves, 4 canonical.
ASSERT_EQ(aGraph.Topo().Edges().Nb(), 8);
BRepGraph_Deduplicate::Options anOpts;
anOpts.AnalyzeOnly = true;
const BRepGraph_Deduplicate::Result aRes = BRepGraph_Deduplicate::Perform(aGraph, anOpts);
EXPECT_EQ(aRes.NbCanonicalCurves, 4);
}
// ---------------------------------------------------------------------------
// Idempotency on various shapes
// ---------------------------------------------------------------------------
TEST(BRepGraph_DeduplicateTest, Idempotent_MixedCompound_SurfacesAndCurves)
{
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes41 =
aGraph.Shapes().Add(makeMixedCompound());
ASSERT_FALSE(aGraph.IsEmpty());
const BRepGraph_Deduplicate::Result aRes1 = BRepGraph_Deduplicate::Perform(aGraph);
ASSERT_EQ(aRes1.NbSurfaceRewrites, 1);
ASSERT_EQ(aRes1.NbCurveRewrites, 4);
// After first dedup, unique pointer counts are stable.
const int aUniqueSurfs = nbUniqueFaceSurfaceDefs(aGraph);
const int aUniqueCurves = nbUniqueEdgeCurveDefs(aGraph);
const BRepGraph_Deduplicate::Result aRes2 = BRepGraph_Deduplicate::Perform(aGraph);
// Unique pointer counts unchanged after second run.
EXPECT_EQ(nbUniqueFaceSurfaceDefs(aGraph), aUniqueSurfs);
EXPECT_EQ(nbUniqueEdgeCurveDefs(aGraph), aUniqueCurves);
}
TEST(BRepGraph_DeduplicateTest, Idempotent_TwoIdenticalBoxes)
{
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes42 =
aGraph.Shapes().Add(makeTwoIdenticalBoxes());
ASSERT_FALSE(aGraph.IsEmpty());
const BRepGraph_Deduplicate::Result aRes1 = BRepGraph_Deduplicate::Perform(aGraph);
ASSERT_EQ(aRes1.NbSurfaceRewrites, 6);
ASSERT_EQ(aRes1.NbCurveRewrites, 12);
// After first dedup, unique pointer counts are stable.
const int aUniqueSurfs = nbUniqueFaceSurfaceDefs(aGraph);
const int aUniqueCurves = nbUniqueEdgeCurveDefs(aGraph);
const BRepGraph_Deduplicate::Result aRes2 = BRepGraph_Deduplicate::Perform(aGraph);
// Unique pointer counts unchanged after second run.
EXPECT_EQ(nbUniqueFaceSurfaceDefs(aGraph), aUniqueSurfs);
EXPECT_EQ(nbUniqueEdgeCurveDefs(aGraph), aUniqueCurves);
}
TEST(BRepGraph_DeduplicateTest, DISABLED_PCurveDedup_RewritesReduceUniquePCurveNodes)
{
BRepPrimAPI_MakeBox aBoxMaker(10.0, 20.0, 30.0);
TopExp_Explorer anExp(aBoxMaker.Shape(), TopAbs_FACE);
BRepBuilderAPI_Copy aCopy(anExp.Current(), true);
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes43 =
aGraph.Shapes().Add(aCopy.Shape());
ASSERT_FALSE(aGraph.IsEmpty());
std::ignore = addDuplicatePCurvesToAllEdges(aGraph);
const uint32_t aUniqueBefore = nbUniquePCurveNodes(aGraph);
const BRepGraph_Deduplicate::Result aRes = BRepGraph_Deduplicate::Perform(aGraph);
// After dedup, fewer unique PCurve node IDs are referenced.
EXPECT_LT(nbUniquePCurveNodes(aGraph), aUniqueBefore);
}
// ---------------------------------------------------------------------------
// Geometry count consistency invariants
// ---------------------------------------------------------------------------
TEST(BRepGraph_DeduplicateTest, GeomCountsUnchanged_AfterDedup)
{
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes46 =
aGraph.Shapes().Add(makeTwoCopiedIdenticalFaces());
ASSERT_FALSE(aGraph.IsEmpty());
// TwoCopiedFaces: 2 surfaces, 8 curves, 8 PCurves.
ASSERT_EQ(aGraph.Topo().Faces().Nb(), 2);
ASSERT_EQ(aGraph.Topo().Edges().Nb(), 8);
ASSERT_EQ(nbPCurveEntries(aGraph), 8);
std::ignore = BRepGraph_Deduplicate::Perform(aGraph);
// Dedup rewrites references, but does not remove geometry nodes.
EXPECT_EQ(aGraph.Topo().Faces().Nb(), 2);
EXPECT_EQ(aGraph.Topo().Edges().Nb(), 8);
EXPECT_EQ(nbPCurveEntries(aGraph), 8);
}
TEST(BRepGraph_DeduplicateTest, DefCountsUnchanged_AfterDedup)
{
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes47 =
aGraph.Shapes().Add(makeTwoCopiedIdenticalFaces());
ASSERT_FALSE(aGraph.IsEmpty());
// TwoCopiedFaces: 2 face defs, 8 edge defs.
ASSERT_EQ(aGraph.Topo().Faces().Nb(), 2);
ASSERT_EQ(aGraph.Topo().Edges().Nb(), 8);
std::ignore = BRepGraph_Deduplicate::Perform(aGraph);
// Dedup never removes or adds defs.
EXPECT_EQ(aGraph.Topo().Faces().Nb(), 2);
EXPECT_EQ(aGraph.Topo().Edges().Nb(), 8);
}
TEST(BRepGraph_DeduplicateTest, PCurveEntryCount_UnchangedAfterDedup)
{
BRepPrimAPI_MakeBox aBoxMaker(10.0, 20.0, 30.0);
TopExp_Explorer anExp(aBoxMaker.Shape(), TopAbs_FACE);
BRepBuilderAPI_Copy aCopy(anExp.Current(), true);
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes48 =
aGraph.Shapes().Add(aCopy.Shape());
ASSERT_FALSE(aGraph.IsEmpty());
std::ignore = addDuplicatePCurvesToAllEdges(aGraph);
const size_t aPCEntryCount = nbPCurveEntries(aGraph);
std::ignore = BRepGraph_Deduplicate::Perform(aGraph);
// Dedup rewrites PCurve node IDs but doesn't add or remove PCurveEntries.
EXPECT_EQ(nbPCurveEntries(aGraph), aPCEntryCount);
}
// ---------------------------------------------------------------------------
// Sphere and cylinder specific tests
// ---------------------------------------------------------------------------
TEST(BRepGraph_DeduplicateTest, TwoCopiedSphereFaces_Deduped)
{
BRepPrimAPI_MakeSphere aSphereMaker(10.0);
const TopoDS_Shape& aSphere = aSphereMaker.Shape();
TopExp_Explorer anExp(aSphere, TopAbs_FACE);
ASSERT_TRUE(anExp.More());
const TopoDS_Shape aFace = anExp.Current();
BRepBuilderAPI_Copy aCopy1(aFace, true);
BRepBuilderAPI_Copy aCopy2(aFace, true);
BRep_Builder aBuilder;
TopoDS_Compound aCompound;
aBuilder.MakeCompound(aCompound);
aBuilder.Add(aCompound, aCopy1.Shape());
aBuilder.Add(aCompound, aCopy2.Shape());
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes49 = aGraph.Shapes().Add(aCompound);
ASSERT_FALSE(aGraph.IsEmpty());
// Two sphere face copies: 2 faces, 6 edges.
ASSERT_EQ(aGraph.Topo().Faces().Nb(), 2);
ASSERT_EQ(aGraph.Topo().Edges().Nb(), 6);
const BRepGraph_Deduplicate::Result aRes = BRepGraph_Deduplicate::Perform(aGraph);
EXPECT_EQ(aRes.NbCanonicalSurfaces, 1);
EXPECT_EQ(aRes.NbSurfaceRewrites, 1);
// After dedup, both sphere faces share the same surface pointer.
EXPECT_EQ(BRepGraph_Tool::Face::Surface(aGraph, BRepGraph_FaceId::Start()).get(),
BRepGraph_Tool::Face::Surface(aGraph, BRepGraph_FaceId(1)).get());
}
TEST(BRepGraph_DeduplicateTest, TwoCopiedCylinderFaces_Deduped)
{
BRepPrimAPI_MakeCylinder aCylMaker(5.0, 20.0);
const TopoDS_Shape& aCyl = aCylMaker.Shape();
TopExp_Explorer anExp(aCyl, TopAbs_FACE);
ASSERT_TRUE(anExp.More());
const TopoDS_Shape aFace = anExp.Current();
BRepBuilderAPI_Copy aCopy1(aFace, true);
BRepBuilderAPI_Copy aCopy2(aFace, true);
BRep_Builder aBuilder;
TopoDS_Compound aCompound;
aBuilder.MakeCompound(aCompound);
aBuilder.Add(aCompound, aCopy1.Shape());
aBuilder.Add(aCompound, aCopy2.Shape());
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes50 = aGraph.Shapes().Add(aCompound);
ASSERT_FALSE(aGraph.IsEmpty());
// Two cylinder face copies: 2 faces, 6 edges, 2 surfaces, 6 curves, 12 PCurves.
ASSERT_EQ(aGraph.Topo().Faces().Nb(), 2);
ASSERT_EQ(aGraph.Topo().Faces().Nb(), 2);
ASSERT_EQ(aGraph.Topo().Edges().Nb(), 6);
const BRepGraph_Deduplicate::Result aRes = BRepGraph_Deduplicate::Perform(aGraph);
EXPECT_EQ(aRes.NbCanonicalSurfaces, 1);
EXPECT_EQ(aRes.NbCanonicalCurves, 3);
EXPECT_EQ(aRes.NbSurfaceRewrites, 1);
EXPECT_EQ(aRes.NbCurveRewrites, 3);
// 8 canonicalize + 1 nullify surface + 3 nullify curves + 4 nullify PCurves = 16.
}
TEST(BRepGraph_DeduplicateTest, DifferentSizedCylinders_NotDeduped)
{
BRepPrimAPI_MakeCylinder aCylMaker1(5.0, 20.0);
BRepPrimAPI_MakeCylinder aCylMaker2(10.0, 20.0);
TopExp_Explorer anExp1(aCylMaker1.Shape(), TopAbs_FACE);
TopExp_Explorer anExp2(aCylMaker2.Shape(), TopAbs_FACE);
ASSERT_TRUE(anExp1.More());
ASSERT_TRUE(anExp2.More());
BRep_Builder aBuilder;
TopoDS_Compound aCompound;
aBuilder.MakeCompound(aCompound);
aBuilder.Add(aCompound, anExp1.Current());
aBuilder.Add(aCompound, anExp2.Current());
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes51 = aGraph.Shapes().Add(aCompound);
ASSERT_FALSE(aGraph.IsEmpty());
// Two distinct cylinder faces: 2 surfaces, 6 curves.
ASSERT_EQ(aGraph.Topo().Faces().Nb(), 2);
ASSERT_EQ(aGraph.Topo().Faces().Nb(), 2);
ASSERT_EQ(aGraph.Topo().Edges().Nb(), 6);
const BRepGraph_Deduplicate::Result aRes = BRepGraph_Deduplicate::Perform(aGraph);
// Different radii -> different cylindrical surfaces -> no surface/curve dedup.
EXPECT_EQ(aRes.NbCanonicalSurfaces, 2);
EXPECT_EQ(aRes.NbCanonicalCurves, 6);
EXPECT_EQ(aRes.NbSurfaceRewrites, 0);
EXPECT_EQ(aRes.NbCurveRewrites, 0);
// No PCurve dedup is performed because PCurves are stored inline.
}
// ---------------------------------------------------------------------------
// Back-reference and orphan nullification tests
// ---------------------------------------------------------------------------
TEST(BRepGraph_DeduplicateTest, BackRefs_SurfaceRewrite_UpdatesFaceDefUsers)
{
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes52 =
aGraph.Shapes().Add(makeTwoCopiedIdenticalFaces());
ASSERT_FALSE(aGraph.IsEmpty());
// Before dedup: each face has its own surface handle.
ASSERT_EQ(aGraph.Topo().Faces().Nb(), 2);
EXPECT_NE(BRepGraph_Tool::Face::Surface(aGraph, BRepGraph_FaceId::Start()).get(),
BRepGraph_Tool::Face::Surface(aGraph, BRepGraph_FaceId(1)).get());
std::ignore = BRepGraph_Deduplicate::Perform(aGraph);
// After dedup: both faces share the same canonical surface pointer.
EXPECT_EQ(BRepGraph_Tool::Face::Surface(aGraph, BRepGraph_FaceId::Start()).get(),
BRepGraph_Tool::Face::Surface(aGraph, BRepGraph_FaceId(1)).get());
}
TEST(BRepGraph_DeduplicateTest, BackRefs_CurveRewrite_UpdatesEdgeDefUsers)
{
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes53 =
aGraph.Shapes().Add(makeTwoCopiedIdenticalFaces());
ASSERT_FALSE(aGraph.IsEmpty());
// Before dedup: 8 edges, each with its own curve handle.
ASSERT_EQ(aGraph.Topo().Edges().Nb(), 8);
std::ignore = BRepGraph_Deduplicate::Perform(aGraph);
// After dedup: edges with identical curves should share the same pointer.
// Count distinct curve pointers across all edges.
NCollection_Map<const Geom_Curve*> aDistinctCurves;
for (BRepGraph_FullEdgeIterator anEdgeIt(aGraph); anEdgeIt.More(); anEdgeIt.Next())
{
const BRepGraph_EdgeId anEdgeId = anEdgeIt.CurrentId();
const occ::handle<Geom_Curve>& aCurve = BRepGraph_Tool::Edge::Curve(aGraph, anEdgeId);
if (!aCurve.IsNull())
{
aDistinctCurves.Add(aCurve.get());
}
}
// After dedup, 4 canonical curves should remain (from 8 originally).
EXPECT_EQ(aDistinctCurves.Extent(), 4);
}
TEST(BRepGraph_DeduplicateTest, FacesOnSurface_AfterDedup_ReturnsCorrectDefs)
{
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes54 =
aGraph.Shapes().Add(makeTwoCopiedIdenticalFaces());
ASSERT_FALSE(aGraph.IsEmpty());
std::ignore = BRepGraph_Deduplicate::Perform(aGraph);
// After dedup, all face defs point to the same canonical surface.
ASSERT_EQ(aGraph.Topo().Faces().Nb(), 2);
EXPECT_TRUE(BRepGraph_Tool::Face::HasSurface(aGraph, BRepGraph_FaceId::Start()));
EXPECT_TRUE(BRepGraph_Tool::Face::HasSurface(aGraph, BRepGraph_FaceId(1)));
EXPECT_EQ(BRepGraph_Tool::Face::Surface(aGraph, BRepGraph_FaceId::Start()).get(),
BRepGraph_Tool::Face::Surface(aGraph, BRepGraph_FaceId(1)).get());
}
TEST(BRepGraph_DeduplicateTest, Nullify_OrphanedSurface_HandleIsNull)
{
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes55 =
aGraph.Shapes().Add(makeTwoCopiedIdenticalFaces());
ASSERT_FALSE(aGraph.IsEmpty());
const BRepGraph_Deduplicate::Result aRes = BRepGraph_Deduplicate::Perform(aGraph);
// No orphan nullification since geometry is stored inline on defs.
EXPECT_EQ(aRes.NbNullifiedSurfaces, 0);
// After dedup, both faces share the same canonical surface; all surfaces non-null.
for (BRepGraph_FullFaceIterator aFaceIt(aGraph); aFaceIt.More(); aFaceIt.Next())
{
const BRepGraph_FaceId aFaceId = aFaceIt.CurrentId();
EXPECT_TRUE(BRepGraph_Tool::Face::HasSurface(aGraph, aFaceId))
<< "Face " << aFaceId.Index << " has null Surface after dedup";
}
}
TEST(BRepGraph_DeduplicateTest, Nullify_OrphanedCurve_HandleIsNull)
{
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes56 =
aGraph.Shapes().Add(makeTwoCopiedIdenticalFaces());
ASSERT_FALSE(aGraph.IsEmpty());
const BRepGraph_Deduplicate::Result aRes = BRepGraph_Deduplicate::Perform(aGraph);
// No orphan nullification since geometry is stored inline on defs.
EXPECT_EQ(aRes.NbNullifiedCurves, 0);
// After dedup, all non-degenerate edges should have non-null Curve3d;
// duplicate edges now share the canonical curve pointer.
for (BRepGraph_FullEdgeIterator anEdgeIt(aGraph); anEdgeIt.More(); anEdgeIt.Next())
{
const BRepGraph_EdgeId anEdgeId = anEdgeIt.CurrentId();
if (!BRepGraph_Tool::Edge::Degenerated(aGraph, anEdgeId))
{
EXPECT_TRUE(BRepGraph_Tool::Edge::HasCurve(aGraph, anEdgeId))
<< "Edge " << anEdgeId.Index << " has null Curve3d after dedup";
}
}
// Verify unique curve count decreased.
EXPECT_EQ(nbUniqueEdgeCurveDefs(aGraph), 4);
}
TEST(BRepGraph_DeduplicateTest, Nullify_OrphanedPCurve_HandleIsNull)
{
BRepPrimAPI_MakeBox aBoxMaker(10.0, 20.0, 30.0);
TopExp_Explorer anExp(aBoxMaker.Shape(), TopAbs_FACE);
BRepBuilderAPI_Copy aCopy(anExp.Current(), true);
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes57 =
aGraph.Shapes().Add(aCopy.Shape());
ASSERT_FALSE(aGraph.IsEmpty());
const int aDupCount = addDuplicatePCurvesToAllEdges(aGraph);
ASSERT_GT(aDupCount, 0);
const BRepGraph_Deduplicate::Result aRes = BRepGraph_Deduplicate::Perform(aGraph);
// After dedup, total PCurve entries may have changed.
// Verify the number of pcurve rewrites is positive (dedup happened).
}
TEST(BRepGraph_DeduplicateTest, AnalyzeOnly_NoBackRefChangesOrNullification)
{
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes58 =
aGraph.Shapes().Add(makeTwoCopiedIdenticalFaces());
ASSERT_FALSE(aGraph.IsEmpty());
// Store surface/curve pointers before deduplication.
NCollection_DynamicArray<const Geom_Surface*> aSurfPtrs;
for (BRepGraph_FullFaceIterator aFaceIt(aGraph); aFaceIt.More(); aFaceIt.Next())
{
aSurfPtrs.Append(BRepGraph_Tool::Face::Surface(aGraph, aFaceIt.CurrentId()).get());
}
NCollection_DynamicArray<const Geom_Curve*> aCurvePtrs;
for (BRepGraph_FullEdgeIterator anEdgeIt(aGraph); anEdgeIt.More(); anEdgeIt.Next())
{
aCurvePtrs.Append(BRepGraph_Tool::Edge::Curve(aGraph, anEdgeIt.CurrentId()).get());
}
BRepGraph_Deduplicate::Options anOpts;
anOpts.AnalyzeOnly = true;
const BRepGraph_Deduplicate::Result aRes = BRepGraph_Deduplicate::Perform(aGraph, anOpts);
EXPECT_EQ(aRes.NbNullifiedSurfaces, 0);
EXPECT_EQ(aRes.NbNullifiedCurves, 0);
// Surface/curve pointers unchanged (analyze only, no rewriting).
for (BRepGraph_FullFaceIterator aFaceIt(aGraph); aFaceIt.More(); aFaceIt.Next())
{
const BRepGraph_FaceId aFaceId = aFaceIt.CurrentId();
EXPECT_EQ(BRepGraph_Tool::Face::Surface(aGraph, aFaceId).get(),
aSurfPtrs.Value(static_cast<int>(aFaceId.Index)));
}
for (BRepGraph_FullEdgeIterator anEdgeIt(aGraph); anEdgeIt.More(); anEdgeIt.Next())
{
const BRepGraph_EdgeId anEdgeId = anEdgeIt.CurrentId();
EXPECT_EQ(BRepGraph_Tool::Edge::Curve(aGraph, anEdgeId).get(),
aCurvePtrs.Value(static_cast<int>(anEdgeId.Index)));
}
// All handles still non-null.
for (BRepGraph_FullFaceIterator aFaceIt(aGraph); aFaceIt.More(); aFaceIt.Next())
{
const BRepGraph_FaceId aFaceId = aFaceIt.CurrentId();
EXPECT_TRUE(BRepGraph_Tool::Face::HasSurface(aGraph, aFaceId));
}
for (BRepGraph_FullEdgeIterator anEdgeIt(aGraph); anEdgeIt.More(); anEdgeIt.Next())
{
const BRepGraph_EdgeId anEdgeId = anEdgeIt.CurrentId();
if (!BRepGraph_Tool::Edge::Degenerated(aGraph, anEdgeId))
{
EXPECT_TRUE(BRepGraph_Tool::Edge::HasCurve(aGraph, anEdgeId));
}
}
}
// ---------------------------------------------------------------------------
// Round-trip tests: Build -> Dedup -> Reconstruct -> Build verifies geometry sharing
// ---------------------------------------------------------------------------
TEST(BRepGraph_DeduplicateTest, RoundTrip_TwoCopiedFaces_FewerSurfaces)
{
const TopoDS_Compound aCompound = makeTwoCopiedIdenticalFaces();
// First graph: build and dedup.
BRepGraph aGraph1;
aGraph1.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes59 =
aGraph1.Shapes().Add(aCompound);
ASSERT_FALSE(aGraph1.IsEmpty());
ASSERT_EQ(aGraph1.Topo().Faces().Nb(), 2);
std::ignore = BRepGraph_Deduplicate::Perform(aGraph1);
ASSERT_EQ(nbUniqueFaceSurfaceDefs(aGraph1), 1);
// Reconstruct each face individually and assemble into a compound.
BRep_Builder aBB;
TopoDS_Compound aReconstructed;
aBB.MakeCompound(aReconstructed);
for (BRepGraph_FullFaceIterator aFaceIt(aGraph1); aFaceIt.More(); aFaceIt.Next())
{
const BRepGraph_FaceId aFaceId = aFaceIt.CurrentId();
const TopoDS_Shape aFace = aGraph1.Shapes().Reconstruct(aFaceId);
ASSERT_FALSE(aFace.IsNull());
aBB.Add(aReconstructed, aFace);
}
// Second graph: build from reconstructed shape.
BRepGraph aGraph2;
aGraph2.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes60 =
aGraph2.Shapes().Add(aReconstructed);
ASSERT_FALSE(aGraph2.IsEmpty());
// Face defs count stays 2 (topology defs, not geometry nodes).
// After round-trip, unique surface pointer count should be 1
// because both faces share the same surface handle from dedup.
EXPECT_EQ(aGraph2.Topo().Faces().Nb(), 2);
EXPECT_EQ(nbUniqueFaceSurfaceDefs(aGraph2), 1);
}
TEST(BRepGraph_DeduplicateTest, RoundTrip_TwoCopiedFaces_FewerCurves)
{
const TopoDS_Compound aCompound = makeTwoCopiedIdenticalFaces();
BRepGraph aGraph1;
aGraph1.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes61 =
aGraph1.Shapes().Add(aCompound);
ASSERT_FALSE(aGraph1.IsEmpty());
ASSERT_EQ(aGraph1.Topo().Edges().Nb(), 8);
std::ignore = BRepGraph_Deduplicate::Perform(aGraph1);
ASSERT_EQ(nbUniqueEdgeCurveDefs(aGraph1), 4);
// Reconstruct each face individually.
BRep_Builder aBB;
TopoDS_Compound aReconstructed;
aBB.MakeCompound(aReconstructed);
for (BRepGraph_FullFaceIterator aFaceIt(aGraph1); aFaceIt.More(); aFaceIt.Next())
{
const BRepGraph_FaceId aFaceId = aFaceIt.CurrentId();
const TopoDS_Shape aFace = aGraph1.Shapes().Reconstruct(aFaceId);
ASSERT_FALSE(aFace.IsNull());
aBB.Add(aReconstructed, aFace);
}
BRepGraph aGraph2;
aGraph2.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes62 =
aGraph2.Shapes().Add(aReconstructed);
ASSERT_FALSE(aGraph2.IsEmpty());
// Edge defs count stays 8 (topology defs, not geometry nodes).
// After round-trip, unique curve pointer count should be 4
// because duplicate edges share the same curve handle from dedup.
EXPECT_EQ(aGraph2.Topo().Edges().Nb(), 8);
EXPECT_EQ(nbUniqueEdgeCurveDefs(aGraph2), 4);
}
TEST(BRepGraph_DeduplicateTest, Build_SharedTFace_OneSurfaceNode)
{
// Create a box and extract two faces that share the same TFace via Same().
BRepPrimAPI_MakeBox aBoxMaker(10.0, 20.0, 30.0);
const TopoDS_Shape& aBox = aBoxMaker.Shape();
TopExp_Explorer anExp(aBox, TopAbs_FACE);
ASSERT_TRUE(anExp.More());
const TopoDS_Face aFace1 = TopoDS::Face(anExp.Current());
// Create a compound with the same face referenced twice (same TShape).
BRep_Builder aBuilder;
TopoDS_Compound aCompound;
aBuilder.MakeCompound(aCompound);
aBuilder.Add(aCompound, aFace1);
aBuilder.Add(aCompound, aFace1);
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes63 = aGraph.Shapes().Add(aCompound);
ASSERT_FALSE(aGraph.IsEmpty());
// Both face usages share the same TFace -> same raw surface pointer -> one surface node.
EXPECT_EQ(aGraph.Topo().Faces().Nb(), 1);
}
TEST(BRepGraph_DeduplicateTest, RoundTrip_TwoBoxes_GeomReduction)
{
const TopoDS_Compound aCompound = makeTwoIdenticalBoxes();
BRepGraph aGraph1;
aGraph1.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes64 =
aGraph1.Shapes().Add(aCompound);
ASSERT_FALSE(aGraph1.IsEmpty());
const uint32_t aSurfsBefore = aGraph1.Topo().Faces().Nb();
const uint32_t aCurvesBefore = aGraph1.Topo().Edges().Nb();
ASSERT_EQ(aSurfsBefore, 12);
ASSERT_EQ(aCurvesBefore, 24);
std::ignore = BRepGraph_Deduplicate::Perform(aGraph1);
// Force reconstruction from deduped graph.
BRepGraph_NodeId aRootId(BRepGraph_NodeId::Kind::Compound, 0);
const TopoDS_Shape aReconstructed = aGraph1.Shapes().Reconstruct(aRootId);
ASSERT_FALSE(aReconstructed.IsNull());
// Build second graph.
BRepGraph aGraph2;
aGraph2.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes65 =
aGraph2.Shapes().Add(aReconstructed);
ASSERT_FALSE(aGraph2.IsEmpty());
// Face/edge def counts stay the same (topology defs, not geometry nodes).
EXPECT_EQ(aGraph2.Topo().Faces().Nb(), aSurfsBefore);
EXPECT_EQ(aGraph2.Topo().Edges().Nb(), aCurvesBefore);
// After dedup, unique geometry pointer counts should reflect deduplication:
// 6 canonical surfaces and 12 canonical curves.
EXPECT_EQ(nbUniqueFaceSurfaceDefs(aGraph2), 6);
EXPECT_EQ(nbUniqueEdgeCurveDefs(aGraph2), 12);
}
// ---------------------------------------------------------------------------
// Topology definition merge tests (MergeEntitiesWhenSafe = true)
// ---------------------------------------------------------------------------
TEST(BRepGraph_DeduplicateTest, MergeVertices_SharedVerticesReduced)
{
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes66 =
aGraph.Shapes().Add(makeTwoCopiedIdenticalFaces());
ASSERT_FALSE(aGraph.IsEmpty());
const uint32_t aNbVerticesBefore = aGraph.Topo().Vertices().Nb();
BRepGraph_Deduplicate::Options anOpts;
anOpts.MergeEntitiesWhenSafe = true;
const BRepGraph_Deduplicate::Result aRes = BRepGraph_Deduplicate::Perform(aGraph, anOpts);
// Two copied faces -> shared vertex positions should merge.
EXPECT_GT(aRes.NbMergedVertices, 0);
// Count non-removed vertices.
int aNbActiveVertices = 0;
for (BRepGraph_VertexIterator aVertexIt(aGraph); aVertexIt.More(); aVertexIt.Next())
{
++aNbActiveVertices;
}
EXPECT_LT(aNbActiveVertices, aNbVerticesBefore);
}
TEST(BRepGraph_DeduplicateTest, MergeEdges_SharedEdgesReduced)
{
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes67 =
aGraph.Shapes().Add(makeTwoCopiedIdenticalFaces());
ASSERT_FALSE(aGraph.IsEmpty());
BRepGraph_Deduplicate::Options anOpts;
anOpts.MergeEntitiesWhenSafe = true;
const BRepGraph_Deduplicate::Result aRes = BRepGraph_Deduplicate::Perform(aGraph, anOpts);
EXPECT_GT(aRes.NbMergedEdges, 0);
}
TEST(BRepGraph_DeduplicateTest, MergeWires_IdenticalWiresMerged)
{
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes68 =
aGraph.Shapes().Add(makeTwoCopiedIdenticalFaces());
ASSERT_FALSE(aGraph.IsEmpty());
BRepGraph_Deduplicate::Options anOpts;
anOpts.MergeEntitiesWhenSafe = true;
const BRepGraph_Deduplicate::Result aRes = BRepGraph_Deduplicate::Perform(aGraph, anOpts);
// After vertex and edge merge, identical wires should also merge.
EXPECT_GE(aRes.NbMergedWires, 0);
}
TEST(BRepGraph_DeduplicateTest, MergeFaces_IdenticalFacesMerged)
{
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes69 =
aGraph.Shapes().Add(makeTwoCopiedIdenticalFaces());
ASSERT_FALSE(aGraph.IsEmpty());
BRepGraph_Deduplicate::Options anOpts;
anOpts.MergeEntitiesWhenSafe = true;
const BRepGraph_Deduplicate::Result aRes = BRepGraph_Deduplicate::Perform(aGraph, anOpts);
// After lower-level merges, faces with same surface+wires should merge.
EXPECT_GE(aRes.NbMergedFaces, 0);
}
TEST(BRepGraph_DeduplicateTest, MergeDefsWhenSafe_False_NoMerge)
{
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes70 =
aGraph.Shapes().Add(makeTwoCopiedIdenticalFaces());
ASSERT_FALSE(aGraph.IsEmpty());
// Default: MergeEntitiesWhenSafe = false.
const BRepGraph_Deduplicate::Result aRes = BRepGraph_Deduplicate::Perform(aGraph);
EXPECT_EQ(aRes.NbMergedVertices, 0);
EXPECT_EQ(aRes.NbMergedEdges, 0);
EXPECT_EQ(aRes.NbMergedWires, 0);
EXPECT_EQ(aRes.NbMergedFaces, 0);
EXPECT_FALSE(aRes.IsEntityMergeApplied);
}
TEST(BRepGraph_DeduplicateTest, AnalyzeOnly_MergeDefsWhenSafe_CountsOnly)
{
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes71 =
aGraph.Shapes().Add(makeTwoCopiedIdenticalFaces());
ASSERT_FALSE(aGraph.IsEmpty());
const uint32_t aNbVerticesBefore = aGraph.Topo().Vertices().Nb();
BRepGraph_Deduplicate::Options anOpts;
anOpts.MergeEntitiesWhenSafe = true;
anOpts.AnalyzeOnly = true;
const BRepGraph_Deduplicate::Result aRes = BRepGraph_Deduplicate::Perform(aGraph, anOpts);
// Counts should be reported.
EXPECT_GT(aRes.NbMergedVertices, 0);
// But no mutation: all vertices still present.
EXPECT_EQ(aGraph.Topo().Vertices().Nb(), aNbVerticesBefore);
EXPECT_FALSE(aRes.IsEntityMergeApplied);
}
TEST(BRepGraph_DeduplicateTest, AfterMerge_Validate_NoIssues)
{
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes73 =
aGraph.Shapes().Add(makeTwoCopiedIdenticalFaces());
ASSERT_FALSE(aGraph.IsEmpty());
BRepGraph_Deduplicate::Options anOpts;
anOpts.MergeEntitiesWhenSafe = true;
std::ignore = BRepGraph_Deduplicate::Perform(aGraph, anOpts);
// After merge + compact, graph should be structurally valid.
// Merge alone may leave stale back-references on geometry nodes
// that are cleaned up by compaction.
std::ignore = BRepGraph_Compact::Perform(aGraph);
const BRepGraph_Validate::Result aValResult = BRepGraph_Validate::Perform(aGraph);
EXPECT_TRUE(aValResult.IsValid());
}
//=================================================================================================
TEST(BRepGraph_DeduplicateTest, MergeFaces_SameSurfaceDifferentWires_NotMerged)
{
// Create two faces on the same planar surface (Z=0) with different boundary wires.
// They share the same underlying surface pointer after dedup canonicalization,
// but have different wire topologies - they should NOT be merged.
// Face 1: a rectangle from (0,0) to (10,20) on Z=0 plane.
gp_Pln aPlane(gp_Pnt(0, 0, 0), gp_Dir(0, 0, 1));
gp_Pnt aP1(0, 0, 0);
gp_Pnt aP2(10, 0, 0);
gp_Pnt aP3(10, 20, 0);
gp_Pnt aP4(0, 20, 0);
BRep_Builder aBuilder;
TopoDS_Wire aWire1;
aBuilder.MakeWire(aWire1);
aBuilder.Add(aWire1, BRepBuilderAPI_MakeEdge(aP1, aP2));
aBuilder.Add(aWire1, BRepBuilderAPI_MakeEdge(aP2, aP3));
aBuilder.Add(aWire1, BRepBuilderAPI_MakeEdge(aP3, aP4));
aBuilder.Add(aWire1, BRepBuilderAPI_MakeEdge(aP4, aP1));
TopoDS_Face aFace1 = BRepBuilderAPI_MakeFace(aPlane, aWire1);
// Face 2: a different rectangle on the same Z=0 plane.
gp_Pnt aQ1(30, 0, 0);
gp_Pnt aQ2(40, 0, 0);
gp_Pnt aQ3(40, 10, 0);
gp_Pnt aQ4(30, 10, 0);
TopoDS_Wire aWire2;
aBuilder.MakeWire(aWire2);
aBuilder.Add(aWire2, BRepBuilderAPI_MakeEdge(aQ1, aQ2));
aBuilder.Add(aWire2, BRepBuilderAPI_MakeEdge(aQ2, aQ3));
aBuilder.Add(aWire2, BRepBuilderAPI_MakeEdge(aQ3, aQ4));
aBuilder.Add(aWire2, BRepBuilderAPI_MakeEdge(aQ4, aQ1));
TopoDS_Face aFace2 = BRepBuilderAPI_MakeFace(aPlane, aWire2);
ASSERT_FALSE(aFace1.IsNull());
ASSERT_FALSE(aFace2.IsNull());
TopoDS_Compound aCompound;
aBuilder.MakeCompound(aCompound);
aBuilder.Add(aCompound, aFace1);
aBuilder.Add(aCompound, aFace2);
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes = aGraph.Shapes().Add(aCompound);
ASSERT_FALSE(aGraph.IsEmpty());
ASSERT_EQ(aGraph.Topo().Faces().Nb(), 2);
BRepGraph_Deduplicate::Options anOpts;
anOpts.MergeEntitiesWhenSafe = true;
const BRepGraph_Deduplicate::Result aRes = BRepGraph_Deduplicate::Perform(aGraph, anOpts);
// Both faces share the same planar surface.
EXPECT_EQ(BRepGraph_Tool::Face::Surface(aGraph, BRepGraph_FaceId::Start()).get(),
BRepGraph_Tool::Face::Surface(aGraph, BRepGraph_FaceId(1)).get());
// But they should NOT be merged because their wires are different.
// Count active (non-removed) faces.
int aNbActiveFaces = 0;
for (BRepGraph_FullFaceIterator aFaceIt(aGraph); aFaceIt.More(); aFaceIt.Next())
{
if (!aFaceIt.CurrentId().IsRemoved(aGraph))
{
++aNbActiveFaces;
}
}
EXPECT_EQ(aNbActiveFaces, 2)
<< "Faces with same surface but different wires were incorrectly merged";
EXPECT_EQ(aRes.NbMergedFaces, 0) << "Faces with different wires should not be merged";
}
// ---------------------------------------------------------------------------
// Edge-case: Sphere seam edge PCurve survival through Dedup+Compact+Reconstruct
// ---------------------------------------------------------------------------
TopoDS_Compound makeThreeCopiedCylinderFaces()
{
BRepPrimAPI_MakeCylinder aCylMaker(5.0, 20.0);
const TopoDS_Shape& aCyl = aCylMaker.Shape();
TopExp_Explorer anExp(aCyl, TopAbs_FACE);
if (!anExp.More())
{
return TopoDS_Compound();
}
const TopoDS_Shape aFace = anExp.Current();
BRepBuilderAPI_Copy aCopy1(aFace, true);
BRepBuilderAPI_Copy aCopy2(aFace, true);
BRepBuilderAPI_Copy aCopy3(aFace, true);
BRep_Builder aBuilder;
TopoDS_Compound aCompound;
aBuilder.MakeCompound(aCompound);
aBuilder.Add(aCompound, aCopy1.Shape());
aBuilder.Add(aCompound, aCopy2.Shape());
aBuilder.Add(aCompound, aCopy3.Shape());
return aCompound;
}
TopoDS_Shape makeFullSphere()
{
return BRepPrimAPI_MakeSphere(10.0).Shape();
}
int countValidCoEdgePCurves(const BRepGraph& theGraph)
{
int aCount = 0;
for (BRepGraph_FullCoEdgeIterator aCEIt(theGraph); aCEIt.More(); aCEIt.Next())
{
const BRepGraph_CoEdgeId aCoEdgeId = aCEIt.CurrentId();
if (!aCoEdgeId.IsRemoved(theGraph)
&& theGraph.Topo().CoEdges().Definition(aCoEdgeId).Curve2DRepId.IsValid())
{
const occ::handle<Geom2d_Curve>& aPCurve =
BRepGraph_Tool::CoEdge::PCurve(theGraph, aCEIt.CurrentId());
if (!aPCurve.IsNull())
{
++aCount;
}
}
}
return aCount;
}
int countSeamCoEdges(const BRepGraph& theGraph)
{
int aCount = 0;
for (BRepGraph_FullCoEdgeIterator aCEIt(theGraph); aCEIt.More(); aCEIt.Next())
{
const BRepGraph_CoEdgeId aCoEdgeId = aCEIt.CurrentId();
const BRepGraphInc::CoEdgeDef& aCE = theGraph.Topo().CoEdges().Definition(aCoEdgeId);
if (!aCoEdgeId.IsRemoved(theGraph) && aCE.FaceId.IsValid() && aCE.ChildEdgeId.IsValid())
{
const NCollection_LinearVector<BRepGraph_CoEdgeId>& aSiblings =
theGraph.Topo().Edges().CoEdges(aCE.ChildEdgeId);
for (const BRepGraph_CoEdgeId& aOtherId : aSiblings)
{
if (aOtherId == aCoEdgeId)
{
continue;
}
const BRepGraphInc::CoEdgeDef& aOther = theGraph.Topo().CoEdges().Definition(aOtherId);
if (!aOtherId.IsRemoved(theGraph) && aOther.FaceId == aCE.FaceId
&& aOther.Orientation != aCE.Orientation)
{
++aCount;
break;
}
}
}
}
return aCount;
}
TEST(BRepGraph_DeduplicateTest, Sphere_SeamPCurve_SurvivesDedupCompactReconstruct)
{
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes =
aGraph.Shapes().Add(makeFullSphere());
ASSERT_FALSE(aGraph.IsEmpty());
// Record PCurve count and seam coedge count before dedup.
const int aPCurvesBefore = countValidCoEdgePCurves(aGraph);
const int aSeamCoEdgesBefore = countSeamCoEdges(aGraph);
ASSERT_EQ(aPCurvesBefore, 4) << "Full sphere must have 4 PCurves";
ASSERT_EQ(aSeamCoEdgesBefore, 2) << "Full sphere must have 2 seam coedges";
// Run full pipeline: dedup (with merge) -> compact.
BRepGraph_Deduplicate::Options anOpts;
anOpts.MergeEntitiesWhenSafe = true;
std::ignore = BRepGraph_Deduplicate::Perform(aGraph, anOpts);
// Verify all PCurves are still non-null after dedup.
for (BRepGraph_FullCoEdgeIterator aCEIt(aGraph); aCEIt.More(); aCEIt.Next())
{
const BRepGraph_CoEdgeId aCoEdgeId = aCEIt.CurrentId();
if (!aCoEdgeId.IsRemoved(aGraph)
&& aGraph.Topo().CoEdges().Definition(aCoEdgeId).Curve2DRepId.IsValid())
{
const occ::handle<Geom2d_Curve>& aPCurve = BRepGraph_Tool::CoEdge::PCurve(aGraph, aCoEdgeId);
EXPECT_FALSE(aPCurve.IsNull())
<< "CoEdge " << aCoEdgeId.Index << " has null PCurve after dedup";
}
}
std::ignore = BRepGraph_Compact::Perform(aGraph);
// Verify all PCurves survived compact.
const int aPCurvesAfter = countValidCoEdgePCurves(aGraph);
EXPECT_EQ(aPCurvesAfter, aPCurvesBefore) << "PCurve count changed after dedup+compact on sphere";
// Verify seam coedges still exist.
const int aSeamCoEdgesAfter = countSeamCoEdges(aGraph);
EXPECT_EQ(aSeamCoEdgesAfter, aSeamCoEdgesBefore)
<< "Seam coedge count changed after dedup+compact";
// Validate graph structure.
const BRepGraph_Validate::Result aVal = BRepGraph_Validate::Perform(aGraph);
EXPECT_TRUE(aVal.IsValid()) << "Graph invalid after dedup+compact on sphere";
// Reconstruct root shape and verify BRep validity.
ASSERT_EQ(aGraph.RootProductIds().Size(), 1);
const TopoDS_Shape aReconstructed =
aGraph.Shapes().Reconstruct(BRepGraph_NodeId(aGraph.RootProductIds().Value(0)));
ASSERT_FALSE(aReconstructed.IsNull());
BRepCheck_Analyzer anAnalyzer(aReconstructed);
EXPECT_TRUE(anAnalyzer.IsValid()) << "Reconstructed sphere is not valid after dedup+compact";
}
TEST(BRepGraph_DeduplicateTest, ThreeCopiedCylinderFaces_PCurvesSurviveFullPipeline)
{
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes =
aGraph.Shapes().Add(makeThreeCopiedCylinderFaces());
ASSERT_FALSE(aGraph.IsEmpty());
ASSERT_EQ(aGraph.Topo().Faces().Nb(), 3);
const uint32_t aEdgesBefore = aGraph.Topo().Edges().Nb();
ASSERT_EQ(aEdgesBefore, 9) << "Three cylinder cap faces: 3 copies * 3 edges each = 9";
ASSERT_EQ(countValidCoEdgePCurves(aGraph), 12)
<< "Three cylinder faces: 3 copies * 4 coedges each = 12 PCurves";
// Run dedup with entity merge.
BRepGraph_Deduplicate::Options anOpts;
anOpts.MergeEntitiesWhenSafe = true;
const BRepGraph_Deduplicate::Result aDedupRes = BRepGraph_Deduplicate::Perform(aGraph, anOpts);
// All surviving coedges must have non-null PCurves.
for (BRepGraph_FullCoEdgeIterator aCEIt(aGraph); aCEIt.More(); aCEIt.Next())
{
const BRepGraph_CoEdgeId aCoEdgeId = aCEIt.CurrentId();
if (!aCoEdgeId.IsRemoved(aGraph)
&& aGraph.Topo().CoEdges().Definition(aCoEdgeId).Curve2DRepId.IsValid())
{
const occ::handle<Geom2d_Curve>& aPCurve = BRepGraph_Tool::CoEdge::PCurve(aGraph, aCoEdgeId);
EXPECT_FALSE(aPCurve.IsNull())
<< "CoEdge " << aCoEdgeId.Index << " PCurve is null after dedup";
}
}
// Compact.
const BRepGraph_Compact::Result aCompactRes = BRepGraph_Compact::Perform(aGraph);
EXPECT_EQ(aCompactRes.NbRemovedEdges, 6)
<< "3 copies merged to 1 face: 3 edges survive, 6 removed by compact";
// All surviving coedges after compact must still have non-null PCurves.
for (BRepGraph_FullCoEdgeIterator aCEIt(aGraph); aCEIt.More(); aCEIt.Next())
{
const BRepGraph_CoEdgeId aCoEdgeId = aCEIt.CurrentId();
if (!aCoEdgeId.IsRemoved(aGraph)
&& aGraph.Topo().CoEdges().Definition(aCoEdgeId).Curve2DRepId.IsValid())
{
const occ::handle<Geom2d_Curve>& aPCurve = BRepGraph_Tool::CoEdge::PCurve(aGraph, aCoEdgeId);
EXPECT_FALSE(aPCurve.IsNull())
<< "CoEdge " << aCoEdgeId.Index << " PCurve is null after compact";
}
}
// Validate graph.
const BRepGraph_Validate::Result aVal = BRepGraph_Validate::Perform(aGraph);
EXPECT_TRUE(aVal.IsValid()) << "Graph invalid after dedup+compact on cylinders";
// Reconstruct each face and validate individually.
for (BRepGraph_FullFaceIterator aFaceIt(aGraph); aFaceIt.More(); aFaceIt.Next())
{
if (aFaceIt.CurrentId().IsRemoved(aGraph))
{
continue;
}
const TopoDS_Shape aFace = aGraph.Shapes().Reconstruct(aFaceIt.CurrentId());
ASSERT_FALSE(aFace.IsNull());
BRepCheck_Analyzer anFA(aFace);
EXPECT_TRUE(anFA.IsValid()) << "Reconstructed face " << aFaceIt.CurrentId().Index
<< " is not valid";
}
}
TopoDS_Compound makeThreeCopiedBoxes()
{
BRepPrimAPI_MakeBox aBoxMaker1(10.0, 20.0, 30.0);
BRepPrimAPI_MakeBox aBoxMaker2(10.0, 20.0, 30.0);
BRepPrimAPI_MakeBox aBoxMaker3(10.0, 20.0, 30.0);
BRep_Builder aBuilder;
TopoDS_Compound aCompound;
aBuilder.MakeCompound(aCompound);
aBuilder.Add(aCompound, aBoxMaker1.Shape());
aBuilder.Add(aCompound, aBoxMaker2.Shape());
aBuilder.Add(aCompound, aBoxMaker3.Shape());
return aCompound;
}
TEST(BRepGraph_DeduplicateTest, ThreeCopiedBoxes_DedupMergeCompact_FacesKeepOuterWires)
{
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes =
aGraph.Shapes().Add(makeThreeCopiedBoxes());
ASSERT_FALSE(aGraph.IsEmpty());
ASSERT_EQ(aGraph.Topo().Faces().Nb(), 18) << "3 boxes * 6 faces each";
ASSERT_EQ(aGraph.Topo().Solids().Nb(), 3);
BRepGraph_Deduplicate::Options anOpts;
anOpts.MergeEntitiesWhenSafe = true;
const BRepGraph_Deduplicate::Result aDedupRes = BRepGraph_Deduplicate::Perform(aGraph, anOpts);
EXPECT_EQ(aDedupRes.NbMergedVertices, 16);
EXPECT_EQ(aDedupRes.NbMergedEdges, 24);
EXPECT_EQ(aDedupRes.NbMergedWires, 12);
EXPECT_EQ(aDedupRes.NbMergedFaces, 12);
// Compact.
const BRepGraph_Compact::Result aCompactRes = BRepGraph_Compact::Perform(aGraph);
EXPECT_EQ(aCompactRes.NbRemovedFaces, 12);
EXPECT_EQ(aCompactRes.NbRemovedEdges, 24);
// Every remaining box face must keep exactly one wire.
for (BRepGraph_FullFaceIterator aFaceIt(aGraph); aFaceIt.More(); aFaceIt.Next())
{
const BRepGraph_FaceId aFaceId = aFaceIt.CurrentId();
if (aFaceId.IsRemoved(aGraph))
{
continue;
}
int aWireCount = 0;
for (const BRepGraph_WireRefId& aRefId : aGraph.Topo().Faces().Relations(aFaceId).WireRefIds)
{
if (!aGraph.Refs().Gen().IsRemoved(aRefId))
{
++aWireCount;
}
}
EXPECT_EQ(aWireCount, 1) << "Face " << aFaceId.Index << " should have exactly 1 wire";
}
// Validate graph.
const BRepGraph_Validate::Result aVal = BRepGraph_Validate::Perform(aGraph);
EXPECT_TRUE(aVal.IsValid()) << "Graph invalid after dedup+compact on 3 boxes";
// Reconstruct remaining solids and validate.
for (BRepGraph_FullSolidIterator aSolidIt(aGraph); aSolidIt.More(); aSolidIt.Next())
{
if (aSolidIt.CurrentId().IsRemoved(aGraph))
{
continue;
}
const TopoDS_Shape aSolid = aGraph.Shapes().Reconstruct(aSolidIt.CurrentId());
ASSERT_FALSE(aSolid.IsNull());
BRepCheck_Analyzer anSA(aSolid);
EXPECT_TRUE(anSA.IsValid()) << "Reconstructed solid " << aSolidIt.CurrentId().Index
<< " is not valid";
}
}
TEST(BRepGraph_DeduplicateTest, SelfLoopEdge_AfterVertexMerge_OrientationCorrect)
{
// A self-loop edge (start == end vertex) should not be marked as reversed
// during edge merge after vertex deduplication.
// Build two identical boxes with separate vertex instances. After vertex
// merge, coincident vertices are canonicalized and the compacted graph must
// retain the same semantic state.
BRepPrimAPI_MakeBox aBoxMaker1(10.0, 10.0, 10.0);
BRepPrimAPI_MakeBox aBoxMaker2(10.0, 10.0, 10.0);
BRep_Builder aBuilder;
TopoDS_Compound aCompound;
aBuilder.MakeCompound(aCompound);
aBuilder.Add(aCompound, aBoxMaker1.Shape());
aBuilder.Add(aCompound, aBoxMaker2.Shape());
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes = aGraph.Shapes().Add(aCompound);
ASSERT_FALSE(aGraph.IsEmpty());
ASSERT_EQ(aGraph.Topo().Solids().Nb(), 2);
BRepGraph_Deduplicate::Options anOpts;
anOpts.MergeEntitiesWhenSafe = true;
std::ignore = BRepGraph_Deduplicate::Perform(aGraph, anOpts);
const BRepGraph_RevisionHash aSemanticHash = BRepGraph_RevisionHash::Hasher::Semantic(aGraph);
// Compact.
std::ignore = BRepGraph_Compact::Perform(aGraph);
EXPECT_EQ(BRepGraph_RevisionHash::Hasher::Semantic(aGraph), aSemanticHash);
// Validate.
const BRepGraph_Validate::Result aVal = BRepGraph_Validate::Perform(aGraph);
EXPECT_TRUE(aVal.IsValid()) << "Graph invalid after dedup+compact on two boxes";
// Reconstruct remaining solids.
for (BRepGraph_FullSolidIterator aSolidIt(aGraph); aSolidIt.More(); aSolidIt.Next())
{
if (aSolidIt.CurrentId().IsRemoved(aGraph))
{
continue;
}
const TopoDS_Shape aSolid = aGraph.Shapes().Reconstruct(aSolidIt.CurrentId());
ASSERT_FALSE(aSolid.IsNull());
EXPECT_TRUE(aSolid.ShapeType() == TopAbs_SOLID);
}
}
// ---------------------------------------------------------------------------
// Edge merge tests
// ---------------------------------------------------------------------------
TEST(BRepGraph_DeduplicateTest, EdgeMerge_ThreeCopiedEdges_GraphValidates)
{
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes =
aGraph.Shapes().Add(makeThreeCopiedIdenticalEdges());
ASSERT_FALSE(aGraph.IsEmpty());
ASSERT_EQ(aGraph.Topo().Edges().Nb(), 3);
BRepGraph_Deduplicate::Options anOpts;
anOpts.MergeEntitiesWhenSafe = true;
const BRepGraph_Deduplicate::Result aDedupRes = BRepGraph_Deduplicate::Perform(aGraph, anOpts);
EXPECT_EQ(aDedupRes.NbMergedEdges, 2);
std::ignore = BRepGraph_Compact::Perform(aGraph);
const BRepGraph_Validate::Result aVal = BRepGraph_Validate::Perform(aGraph);
EXPECT_TRUE(aVal.IsValid());
}
TEST(BRepGraph_DeduplicateTest, EdgeMerge_ReversedEdges_MergeAndValidate)
{
BRepGraph aGraph;
aGraph.Clear();
addTwoReversedEdgesOnSharedCurve(aGraph);
ASSERT_FALSE(aGraph.IsEmpty());
ASSERT_EQ(aGraph.Topo().Edges().Nb(), 2);
BRepGraph_Deduplicate::Options anOpts;
anOpts.MergeEntitiesWhenSafe = true;
const BRepGraph_Deduplicate::Result aDedupRes = BRepGraph_Deduplicate::Perform(aGraph, anOpts);
EXPECT_EQ(aDedupRes.NbMergedEdges, 1);
std::ignore = BRepGraph_Compact::Perform(aGraph);
int aNbActiveEdges = 0;
for (BRepGraph_FullEdgeIterator anEdgeIt(aGraph); anEdgeIt.More(); anEdgeIt.Next())
{
if (!anEdgeIt.CurrentId().IsRemoved(aGraph))
{
++aNbActiveEdges;
}
}
EXPECT_EQ(aNbActiveEdges, 1);
const BRepGraph_Validate::Result aVal = BRepGraph_Validate::Perform(aGraph);
EXPECT_TRUE(aVal.IsValid());
}
// ---------------------------------------------------------------------------
// Face merge tests
// ---------------------------------------------------------------------------
TEST(BRepGraph_DeduplicateTest, FaceMerge_TwoIdenticalSingularFaces_Merge)
{
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes =
aGraph.Shapes().Add(makeTwoCopiedIdenticalFaces());
ASSERT_FALSE(aGraph.IsEmpty());
ASSERT_EQ(aGraph.Topo().Faces().Nb(), 2);
BRepGraph_Deduplicate::Options anOpts;
anOpts.MergeEntitiesWhenSafe = true;
const BRepGraph_Deduplicate::Result aDedupRes = BRepGraph_Deduplicate::Perform(aGraph, anOpts);
EXPECT_EQ(aDedupRes.NbMergedFaces, 1);
std::ignore = BRepGraph_Compact::Perform(aGraph);
int aNbActiveFaces = 0;
for (BRepGraph_FullFaceIterator aFaceIt(aGraph); aFaceIt.More(); aFaceIt.Next())
{
if (!aFaceIt.CurrentId().IsRemoved(aGraph))
{
++aNbActiveFaces;
}
}
EXPECT_EQ(aNbActiveFaces, 1);
const BRepGraph_Validate::Result aVal = BRepGraph_Validate::Perform(aGraph);
EXPECT_TRUE(aVal.IsValid());
}
TEST(BRepGraph_DeduplicateTest, FaceMerge_TwoIdenticalInnerWireFaces_Merge)
{
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes =
aGraph.Shapes().Add(makeTwoCopiedFacesWithInnerWire());
ASSERT_FALSE(aGraph.IsEmpty());
ASSERT_EQ(aGraph.Topo().Faces().Nb(), 2);
bool hasInnerWire = false;
for (BRepGraph_FullFaceIterator aFaceIt(aGraph); aFaceIt.More(); aFaceIt.Next())
{
if (aGraph.Topo().Faces().Relations(aFaceIt.CurrentId()).WireRefIds.Size() > 1)
{
hasInnerWire = true;
break;
}
}
ASSERT_TRUE(hasInnerWire);
BRepGraph_Deduplicate::Options anOpts;
anOpts.MergeEntitiesWhenSafe = true;
const BRepGraph_Deduplicate::Result aDedupRes = BRepGraph_Deduplicate::Perform(aGraph, anOpts);
EXPECT_EQ(aDedupRes.NbMergedFaces, 1);
std::ignore = BRepGraph_Compact::Perform(aGraph);
int aNbActiveFaces = 0;
for (BRepGraph_FullFaceIterator aFaceIt(aGraph); aFaceIt.More(); aFaceIt.Next())
{
if (!aFaceIt.CurrentId().IsRemoved(aGraph))
{
++aNbActiveFaces;
EXPECT_EQ(aGraph.Topo().Faces().Relations(aFaceIt.CurrentId()).WireRefIds.Size(), 2);
}
}
EXPECT_EQ(aNbActiveFaces, 1);
const BRepGraph_Validate::Result aVal = BRepGraph_Validate::Perform(aGraph);
EXPECT_TRUE(aVal.IsValid());
}
// ---------------------------------------------------------------------------
// Edge-case: edge face counts after merge on face-touching boxes
// ---------------------------------------------------------------------------
TEST(BRepGraph_DeduplicateTest, DedupMerge_TwoBoxesWithSharedEdge_EdgeFaceCountCorrect)
{
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes =
aGraph.Shapes().Add(makeTwoFaceTouchingBoxes());
ASSERT_FALSE(aGraph.IsEmpty());
BRepGraph_Deduplicate::Options anOpts;
anOpts.MergeEntitiesWhenSafe = true;
std::ignore = BRepGraph_Deduplicate::Perform(aGraph, anOpts);
uint32_t aNbEdgesWith2Faces = 0;
for (BRepGraph_EdgeIterator anEdgeIt(aGraph); anEdgeIt.More(); anEdgeIt.Next())
{
const BRepGraph_EdgeId anEdgeId = anEdgeIt.CurrentId();
const NCollection_LinearVector<BRepGraph_CoEdgeId>& aCoEdgeIdxs =
aGraph.Topo().Edges().CoEdges(anEdgeId);
NCollection_FlatMap<BRepGraph_FaceId> aFaceSet;
for (size_t aCEIdx = 0; aCEIdx < aCoEdgeIdxs.Size(); ++aCEIdx)
{
const BRepGraph_CoEdgeId aCEId = aCoEdgeIdxs.Value(aCEIdx);
const BRepGraphInc::CoEdgeDef& aCE = aGraph.Topo().CoEdges().Definition(aCEId);
if (!aCEId.IsRemoved(aGraph) && aCE.FaceId.IsValid())
{
aFaceSet.Add(aCE.FaceId);
}
}
const uint32_t aNbFaces = static_cast<uint32_t>(aFaceSet.Size());
if (aNbFaces == 2)
{
++aNbEdgesWith2Faces;
}
}
// Two face-touching boxes share 4 edges on the common face.
// After merge, edges from both boxes on the shared plane collapse,
// giving those edges 2 faces. Additional edges also have 2 faces
// since each edge within a box is shared by 2 faces.
EXPECT_EQ(aNbEdgesWith2Faces, 16);
// Compact and validate.
std::ignore = BRepGraph_Compact::Perform(aGraph);
const BRepGraph_Validate::Result aVal = BRepGraph_Validate::Perform(aGraph);
EXPECT_TRUE(aVal.IsValid());
}
// ---------------------------------------------------------------------------
// Edge-case: different-sized spheres should not be merged
// ---------------------------------------------------------------------------
TEST(BRepGraph_DeduplicateTest, DedupMerge_TwoSpheres_DifferentSize_NotMerged)
{
BRepPrimAPI_MakeSphere aSphereMaker1(5.0);
BRepPrimAPI_MakeSphere aSphereMaker2(10.0);
BRep_Builder aBuilder;
TopoDS_Compound aCompound;
aBuilder.MakeCompound(aCompound);
aBuilder.Add(aCompound, aSphereMaker1.Shape());
aBuilder.Add(aCompound, aSphereMaker2.Shape());
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes = aGraph.Shapes().Add(aCompound);
ASSERT_FALSE(aGraph.IsEmpty());
BRepGraph_Deduplicate::Options anOpts;
anOpts.MergeEntitiesWhenSafe = true;
const BRepGraph_Deduplicate::Result aRes = BRepGraph_Deduplicate::Perform(aGraph, anOpts);
EXPECT_EQ(aRes.NbMergedFaces, 0);
EXPECT_EQ(aRes.NbMergedEdges, 0);
int aNbActiveFaces = 0;
for (BRepGraph_FullFaceIterator aFaceIt(aGraph); aFaceIt.More(); aFaceIt.Next())
{
if (!aFaceIt.CurrentId().IsRemoved(aGraph))
{
++aNbActiveFaces;
}
}
EXPECT_EQ(aNbActiveFaces, 2);
}
// ---------------------------------------------------------------------------
// Edge-case: three identical boxes - explicit merge count assertions
// ---------------------------------------------------------------------------
TEST(BRepGraph_DeduplicateTest, DedupMerge_ThreeBoxes_AllEdgesMergeCorrectly)
{
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes =
aGraph.Shapes().Add(makeThreeCopiedBoxes());
ASSERT_FALSE(aGraph.IsEmpty());
BRepGraph_Deduplicate::Options anOpts;
anOpts.MergeEntitiesWhenSafe = true;
const BRepGraph_Deduplicate::Result aDedupRes = BRepGraph_Deduplicate::Perform(aGraph, anOpts);
EXPECT_EQ(aDedupRes.NbMergedVertices, 16);
EXPECT_EQ(aDedupRes.NbMergedEdges, 24);
EXPECT_EQ(aDedupRes.NbMergedWires, 12);
EXPECT_EQ(aDedupRes.NbMergedFaces, 12);
const BRepGraph_Compact::Result aCompactRes = BRepGraph_Compact::Perform(aGraph);
EXPECT_EQ(aCompactRes.NbRemovedFaces, 12);
EXPECT_EQ(aCompactRes.NbRemovedEdges, 24);
const BRepGraph_Validate::Result aVal = BRepGraph_Validate::Perform(aGraph);
EXPECT_TRUE(aVal.IsValid());
for (BRepGraph_FullFaceIterator aFaceIt(aGraph); aFaceIt.More(); aFaceIt.Next())
{
const BRepGraph_FaceId aFaceId = aFaceIt.CurrentId();
if (aFaceId.IsRemoved(aGraph))
{
continue;
}
int aWireCount = 0;
for (const BRepGraph_WireRefId& aRefId : aGraph.Topo().Faces().Relations(aFaceId).WireRefIds)
{
if (!aGraph.Refs().Gen().IsRemoved(aRefId))
{
++aWireCount;
}
}
EXPECT_EQ(aWireCount, 1);
}
}
// ---------------------------------------------------------------------------
// Edge-case: sphere self-loop / degenerate edge orientation after merge
// ---------------------------------------------------------------------------
TEST(BRepGraph_DeduplicateTest, DedupMerge_SelfLoopEdge_OrientationPreserved)
{
BRepGraph aGraph;
aGraph.Clear();
[[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes =
aGraph.Shapes().Add(makeFullSphere());
ASSERT_FALSE(aGraph.IsEmpty());
BRepGraph_Deduplicate::Options anOpts;
anOpts.MergeEntitiesWhenSafe = true;
std::ignore = BRepGraph_Deduplicate::Perform(aGraph, anOpts);
std::ignore = BRepGraph_Compact::Perform(aGraph);
const BRepGraph_Validate::Result aVal = BRepGraph_Validate::Perform(aGraph);
EXPECT_TRUE(aVal.IsValid());
ASSERT_EQ(aGraph.RootProductIds().Size(), 1);
const TopoDS_Shape aReconstructed =
aGraph.Shapes().Reconstruct(BRepGraph_NodeId(aGraph.RootProductIds().Value(0)));
ASSERT_FALSE(aReconstructed.IsNull());
BRepCheck_Analyzer anAnalyzer(aReconstructed);
EXPECT_TRUE(anAnalyzer.IsValid());
}