// 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 #include #include #include #include #include #include #include #include #include #include "BRepGraph_RefTestTools.hxx" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static_assert(std::is_same_v); static_assert(std::is_same_v); static_assert(std::is_same_v); static double computeArea(const TopoDS_Shape& theShape) { GProp_GProps aProps; BRepGProp::SurfaceProperties(theShape, aProps); return aProps.Mass(); } static double computeVolume(const TopoDS_Shape& theShape) { GProp_GProps aProps; BRepGProp::VolumeProperties(theShape, aProps); return aProps.Mass(); } static int countSubShapes(const TopoDS_Shape& theShape, TopAbs_ShapeEnum theType) { int aCount = 0; for (TopExp_Explorer anExp(theShape, theType); anExp.More(); anExp.Next()) ++aCount; return aCount; } // ============================================================ // Entity count validation // ============================================================ TEST(BRepGraphIncTest, Box_EntityCounts_MatchDefCounts) { BRepPrimAPI_MakeBox aBoxMaker(10.0, 20.0, 30.0); const TopoDS_Shape& aBox = aBoxMaker.Shape(); // Build BRepGraph for parity checks. BRepGraph aGraph; aGraph.Clear(); [[maybe_unused]] const BRepGraph_Builder::Result aBuildRes1 = BRepGraph_Builder::Add(aGraph, aBox); ASSERT_TRUE(aGraph.IsDone()); // Build incidence storage. BRepGraphInc_Storage aStorage; BRepGraphInc_Populate::Perform(aStorage, aBox, false); ASSERT_TRUE(aStorage.GetIsDone()); // Entity counts must match Def counts. EXPECT_EQ(aStorage.NbVertices(), aGraph.Topo().Vertices().Nb()); EXPECT_EQ(aStorage.NbEdges(), aGraph.Topo().Edges().Nb()); EXPECT_EQ(aStorage.NbWires(), aGraph.Topo().Wires().Nb()); EXPECT_EQ(aStorage.NbFaces(), aGraph.Topo().Faces().Nb()); EXPECT_EQ(aStorage.NbShells(), aGraph.Topo().Shells().Nb()); EXPECT_EQ(aStorage.NbSolids(), aGraph.Topo().Solids().Nb()); } TEST(BRepGraphIncTest, Cylinder_EntityCounts_MatchDefCounts) { BRepPrimAPI_MakeCylinder aCylMaker(5.0, 15.0); const TopoDS_Shape& aCyl = aCylMaker.Shape(); BRepGraph aGraph; aGraph.Clear(); [[maybe_unused]] const BRepGraph_Builder::Result aBuildRes2 = BRepGraph_Builder::Add(aGraph, aCyl); ASSERT_TRUE(aGraph.IsDone()); BRepGraphInc_Storage aStorage; BRepGraphInc_Populate::Perform(aStorage, aCyl, false); ASSERT_TRUE(aStorage.GetIsDone()); EXPECT_EQ(aStorage.NbVertices(), aGraph.Topo().Vertices().Nb()); EXPECT_EQ(aStorage.NbEdges(), aGraph.Topo().Edges().Nb()); EXPECT_EQ(aStorage.NbWires(), aGraph.Topo().Wires().Nb()); EXPECT_EQ(aStorage.NbFaces(), aGraph.Topo().Faces().Nb()); EXPECT_EQ(aStorage.NbShells(), aGraph.Topo().Shells().Nb()); EXPECT_EQ(aStorage.NbSolids(), aGraph.Topo().Solids().Nb()); } TEST(BRepGraphIncTest, Sphere_EntityCounts_MatchDefCounts) { BRepPrimAPI_MakeSphere aSphMaker(8.0); const TopoDS_Shape& aSph = aSphMaker.Shape(); BRepGraph aGraph; aGraph.Clear(); [[maybe_unused]] const BRepGraph_Builder::Result aBuildRes3 = BRepGraph_Builder::Add(aGraph, aSph); ASSERT_TRUE(aGraph.IsDone()); BRepGraphInc_Storage aStorage; BRepGraphInc_Populate::Perform(aStorage, aSph, false); ASSERT_TRUE(aStorage.GetIsDone()); EXPECT_EQ(aStorage.NbVertices(), aGraph.Topo().Vertices().Nb()); EXPECT_EQ(aStorage.NbEdges(), aGraph.Topo().Edges().Nb()); EXPECT_EQ(aStorage.NbWires(), aGraph.Topo().Wires().Nb()); EXPECT_EQ(aStorage.NbFaces(), aGraph.Topo().Faces().Nb()); } TEST(BRepGraphIncTest, Storage_MarkRemovedRep_DecrementsActiveCountsAndIsIdempotent) { BRepGraphInc_Storage aStorage; (void)aStorage.AppendSurfaceRep(); (void)aStorage.AppendCurve3DRep(); (void)aStorage.AppendCurve2DRep(); (void)aStorage.AppendTriangulationRep(); (void)aStorage.AppendPolygon3DRep(); (void)aStorage.AppendPolygon2DRep(); (void)aStorage.AppendPolygonOnTriRep(); EXPECT_EQ(aStorage.NbActiveSurfaces(), 1); EXPECT_EQ(aStorage.NbActiveCurves3D(), 1); EXPECT_EQ(aStorage.NbActiveCurves2D(), 1); EXPECT_EQ(aStorage.NbActiveTriangulations(), 1); EXPECT_EQ(aStorage.NbActivePolygons3D(), 1); EXPECT_EQ(aStorage.NbActivePolygons2D(), 1); EXPECT_EQ(aStorage.NbActivePolygonsOnTri(), 1); EXPECT_TRUE(aStorage.MarkRemovedRep(BRepGraph_SurfaceRepId::Start())); EXPECT_TRUE(aStorage.MarkRemovedRep(BRepGraph_Curve3DRepId::Start())); EXPECT_TRUE(aStorage.MarkRemovedRep(BRepGraph_Curve2DRepId::Start())); EXPECT_TRUE(aStorage.MarkRemovedRep(BRepGraph_TriangulationRepId::Start())); EXPECT_TRUE(aStorage.MarkRemovedRep(BRepGraph_Polygon3DRepId::Start())); EXPECT_TRUE(aStorage.MarkRemovedRep(BRepGraph_Polygon2DRepId::Start())); EXPECT_TRUE(aStorage.MarkRemovedRep(BRepGraph_PolygonOnTriRepId::Start())); EXPECT_TRUE(aStorage.SurfaceRep(BRepGraph_SurfaceRepId::Start()).IsRemoved); EXPECT_TRUE(aStorage.Curve3DRep(BRepGraph_Curve3DRepId::Start()).IsRemoved); EXPECT_TRUE(aStorage.Curve2DRep(BRepGraph_Curve2DRepId::Start()).IsRemoved); EXPECT_TRUE(aStorage.TriangulationRep(BRepGraph_TriangulationRepId::Start()).IsRemoved); EXPECT_TRUE(aStorage.Polygon3DRep(BRepGraph_Polygon3DRepId::Start()).IsRemoved); EXPECT_TRUE(aStorage.Polygon2DRep(BRepGraph_Polygon2DRepId::Start()).IsRemoved); EXPECT_TRUE(aStorage.PolygonOnTriRep(BRepGraph_PolygonOnTriRepId::Start()).IsRemoved); EXPECT_EQ(aStorage.NbActiveSurfaces(), 0); EXPECT_EQ(aStorage.NbActiveCurves3D(), 0); EXPECT_EQ(aStorage.NbActiveCurves2D(), 0); EXPECT_EQ(aStorage.NbActiveTriangulations(), 0); EXPECT_EQ(aStorage.NbActivePolygons3D(), 0); EXPECT_EQ(aStorage.NbActivePolygons2D(), 0); EXPECT_EQ(aStorage.NbActivePolygonsOnTri(), 0); EXPECT_FALSE(aStorage.MarkRemovedRep(BRepGraph_SurfaceRepId::Start())); EXPECT_FALSE(aStorage.MarkRemovedRep(BRepGraph_Curve3DRepId::Start())); EXPECT_FALSE(aStorage.MarkRemovedRep(BRepGraph_Curve2DRepId::Start())); EXPECT_FALSE(aStorage.MarkRemovedRep(BRepGraph_TriangulationRepId::Start())); EXPECT_FALSE(aStorage.MarkRemovedRep(BRepGraph_Polygon3DRepId::Start())); EXPECT_FALSE(aStorage.MarkRemovedRep(BRepGraph_Polygon2DRepId::Start())); EXPECT_FALSE(aStorage.MarkRemovedRep(BRepGraph_PolygonOnTriRepId::Start())); EXPECT_FALSE(aStorage.MarkRemovedRep(BRepGraph_RepId())); } TEST(BRepGraphIncTest, Storage_AppendAccess_UsesTypedIds) { BRepGraphInc_Storage aStorage; const BRepGraph_VertexId aVertexId = aStorage.AppendVertex(); const BRepGraph_FaceRefId aFaceRefId = aStorage.AppendFaceRef(); const BRepGraph_SurfaceRepId aSurfaceRepId = aStorage.AppendSurfaceRep(); EXPECT_EQ(aVertexId.Index, 0); EXPECT_EQ(aFaceRefId.Index, 0); EXPECT_EQ(aSurfaceRepId.Index, 0); EXPECT_FALSE(aStorage.Vertex(aVertexId).IsRemoved); EXPECT_FALSE(aStorage.FaceRef(aFaceRefId).IsRemoved); EXPECT_FALSE(aStorage.SurfaceRep(aSurfaceRepId).IsRemoved); aStorage.ChangeVertex(aVertexId).Tolerance = 1.25; aStorage.ChangeFaceRef(aFaceRefId).Orientation = TopAbs_REVERSED; aStorage.ChangeSurfaceRep(aSurfaceRepId).OwnGen = 7; EXPECT_DOUBLE_EQ(aStorage.Vertex(aVertexId).Tolerance, 1.25); EXPECT_EQ(aStorage.FaceRef(aFaceRefId).Orientation, TopAbs_REVERSED); EXPECT_EQ(aStorage.SurfaceRep(aSurfaceRepId).OwnGen, 7u); } TEST(BRepGraphIncTest, Storage_GenericIdDispatch_UsesTypedHelpers) { BRepGraphInc_Storage aStorage; const BRepGraph_VertexId aVertexId = aStorage.AppendVertex(); const BRepGraph_FaceRefId aFaceRefId = aStorage.AppendFaceRef(); const BRepGraph_SurfaceRepId aSurfaceRepId = aStorage.AppendSurfaceRep(); aStorage.ChangeBaseRef(BRepGraph_RefId(aFaceRefId)).OwnGen = 11; EXPECT_EQ(aStorage.BaseRef(BRepGraph_RefId(aFaceRefId)).OwnGen, 11u); EXPECT_TRUE(aStorage.MarkRemoved(BRepGraph_NodeId(aVertexId))); EXPECT_TRUE(aStorage.MarkRemovedRef(BRepGraph_RefId(aFaceRefId))); EXPECT_TRUE(aStorage.MarkRemovedRep(BRepGraph_RepId(aSurfaceRepId))); EXPECT_TRUE(aStorage.Vertex(aVertexId).IsRemoved); EXPECT_TRUE(aStorage.FaceRef(aFaceRefId).IsRemoved); EXPECT_TRUE(aStorage.SurfaceRep(aSurfaceRepId).IsRemoved); EXPECT_EQ(aStorage.NbActiveVertices(), 0); EXPECT_EQ(aStorage.NbActiveFaceRefs(), 0); EXPECT_EQ(aStorage.NbActiveSurfaces(), 0); } // ============================================================ // Round-trip: area preservation // ============================================================ TEST(BRepGraphIncTest, Box_RoundTrip_AreaPreserved) { BRepPrimAPI_MakeBox aBoxMaker(10.0, 20.0, 30.0); const TopoDS_Shape& aBox = aBoxMaker.Shape(); const double anOrigArea = computeArea(aBox); BRepGraphInc_Storage aStorage; BRepGraphInc_Populate::Perform(aStorage, aBox, false); ASSERT_TRUE(aStorage.GetIsDone()); TopoDS_Shape aRecon = BRepGraphInc_Reconstruct::Node(aStorage, BRepGraph_SolidId::Start()); ASSERT_FALSE(aRecon.IsNull()); const double aReconArea = computeArea(aRecon); EXPECT_NEAR(aReconArea, anOrigArea, Precision::Confusion()); } TEST(BRepGraphIncTest, Cylinder_RoundTrip_AreaPreserved) { BRepPrimAPI_MakeCylinder aCylMaker(5.0, 15.0); const TopoDS_Shape& aCyl = aCylMaker.Shape(); const double anOrigArea = computeArea(aCyl); BRepGraphInc_Storage aStorage; BRepGraphInc_Populate::Perform(aStorage, aCyl, false); ASSERT_TRUE(aStorage.GetIsDone()); TopoDS_Shape aRecon = BRepGraphInc_Reconstruct::Node(aStorage, BRepGraph_SolidId::Start()); ASSERT_FALSE(aRecon.IsNull()); const double aReconArea = computeArea(aRecon); EXPECT_NEAR(aReconArea, anOrigArea, Precision::Confusion()); } TEST(BRepGraphIncTest, Sphere_RoundTrip_AreaPreserved) { BRepPrimAPI_MakeSphere aSphMaker(8.0); const TopoDS_Shape& aSph = aSphMaker.Shape(); const double anOrigArea = computeArea(aSph); BRepGraphInc_Storage aStorage; BRepGraphInc_Populate::Perform(aStorage, aSph, false); ASSERT_TRUE(aStorage.GetIsDone()); TopoDS_Shape aRecon = BRepGraphInc_Reconstruct::Node(aStorage, BRepGraph_SolidId::Start()); ASSERT_FALSE(aRecon.IsNull()); const double aReconArea = computeArea(aRecon); EXPECT_NEAR(aReconArea, anOrigArea, Precision::Confusion()); } // ============================================================ // Round-trip: volume preservation // ============================================================ TEST(BRepGraphIncTest, Box_RoundTrip_VolumePreserved) { BRepPrimAPI_MakeBox aBoxMaker(10.0, 20.0, 30.0); const TopoDS_Shape& aBox = aBoxMaker.Shape(); const double anOrigVol = computeVolume(aBox); BRepGraphInc_Storage aStorage; BRepGraphInc_Populate::Perform(aStorage, aBox, false); ASSERT_TRUE(aStorage.GetIsDone()); TopoDS_Shape aRecon = BRepGraphInc_Reconstruct::Node(aStorage, BRepGraph_SolidId::Start()); ASSERT_FALSE(aRecon.IsNull()); const double aReconVol = computeVolume(aRecon); EXPECT_NEAR(aReconVol, anOrigVol, Precision::Confusion()); } TEST(BRepGraphIncTest, Cylinder_RoundTrip_VolumePreserved) { BRepPrimAPI_MakeCylinder aCylMaker(5.0, 15.0); const TopoDS_Shape& aCyl = aCylMaker.Shape(); const double anOrigVol = computeVolume(aCyl); BRepGraphInc_Storage aStorage; BRepGraphInc_Populate::Perform(aStorage, aCyl, false); ASSERT_TRUE(aStorage.GetIsDone()); TopoDS_Shape aRecon = BRepGraphInc_Reconstruct::Node(aStorage, BRepGraph_SolidId::Start()); ASSERT_FALSE(aRecon.IsNull()); const double aReconVol = computeVolume(aRecon); EXPECT_NEAR(aReconVol, anOrigVol, Precision::Confusion()); } // ============================================================ // Round-trip: sub-shape counts // ============================================================ TEST(BRepGraphIncTest, Box_RoundTrip_SubShapeCounts) { BRepPrimAPI_MakeBox aBoxMaker(10.0, 20.0, 30.0); const TopoDS_Shape& aBox = aBoxMaker.Shape(); BRepGraphInc_Storage aStorage; BRepGraphInc_Populate::Perform(aStorage, aBox, false); ASSERT_TRUE(aStorage.GetIsDone()); TopoDS_Shape aRecon = BRepGraphInc_Reconstruct::Node(aStorage, BRepGraph_SolidId::Start()); ASSERT_FALSE(aRecon.IsNull()); EXPECT_EQ(countSubShapes(aRecon, TopAbs_FACE), countSubShapes(aBox, TopAbs_FACE)); EXPECT_EQ(countSubShapes(aRecon, TopAbs_WIRE), countSubShapes(aBox, TopAbs_WIRE)); EXPECT_EQ(countSubShapes(aRecon, TopAbs_EDGE), countSubShapes(aBox, TopAbs_EDGE)); EXPECT_EQ(countSubShapes(aRecon, TopAbs_VERTEX), countSubShapes(aBox, TopAbs_VERTEX)); } TEST(BRepGraphIncTest, Cylinder_RoundTrip_SubShapeCounts) { BRepPrimAPI_MakeCylinder aCylMaker(5.0, 15.0); const TopoDS_Shape& aCyl = aCylMaker.Shape(); BRepGraphInc_Storage aStorage; BRepGraphInc_Populate::Perform(aStorage, aCyl, false); ASSERT_TRUE(aStorage.GetIsDone()); TopoDS_Shape aRecon = BRepGraphInc_Reconstruct::Node(aStorage, BRepGraph_SolidId::Start()); ASSERT_FALSE(aRecon.IsNull()); EXPECT_EQ(countSubShapes(aRecon, TopAbs_FACE), countSubShapes(aCyl, TopAbs_FACE)); EXPECT_EQ(countSubShapes(aRecon, TopAbs_WIRE), countSubShapes(aCyl, TopAbs_WIRE)); EXPECT_EQ(countSubShapes(aRecon, TopAbs_EDGE), countSubShapes(aCyl, TopAbs_EDGE)); EXPECT_EQ(countSubShapes(aRecon, TopAbs_VERTEX), countSubShapes(aCyl, TopAbs_VERTEX)); } // ============================================================ // Reverse index consistency // ============================================================ TEST(BRepGraphIncTest, Box_ReverseIndex_EdgesToWires) { BRepPrimAPI_MakeBox aBoxMaker(10.0, 20.0, 30.0); const TopoDS_Shape& aBox = aBoxMaker.Shape(); BRepGraphInc_Storage aStorage; BRepGraphInc_Populate::Perform(aStorage, aBox, false); ASSERT_TRUE(aStorage.GetIsDone()); // Every edge must appear in at least one wire. const int aNbEdges = aStorage.NbEdges(); for (BRepGraph_EdgeId anEdgeId(0); anEdgeId.IsValid(aNbEdges); ++anEdgeId) { const NCollection_DynamicArray* aWires = aStorage.ReverseIndex().WiresOfEdge(anEdgeId); EXPECT_TRUE(aWires != nullptr) << "Edge " << anEdgeId.Index << " not in any wire"; if (aWires != nullptr) { EXPECT_GE(aWires->Length(), 1); } } } TEST(BRepGraphIncTest, Box_ReverseIndex_EdgesToFaces) { BRepPrimAPI_MakeBox aBoxMaker(10.0, 20.0, 30.0); const TopoDS_Shape& aBox = aBoxMaker.Shape(); BRepGraphInc_Storage aStorage; BRepGraphInc_Populate::Perform(aStorage, aBox, false); ASSERT_TRUE(aStorage.GetIsDone()); // Every edge must appear in at least one face (via EdgeFaceGeom). const int aNbEdges = aStorage.NbEdges(); for (BRepGraph_EdgeId anEdgeId(0); anEdgeId.IsValid(aNbEdges); ++anEdgeId) { if (aStorage.Edge(anEdgeId).IsDegenerate) continue; const NCollection_DynamicArray* aFaces = aStorage.ReverseIndex().FacesOfEdge(anEdgeId); EXPECT_TRUE(aFaces != nullptr) << "Edge " << anEdgeId.Index << " not in any face"; if (aFaces != nullptr) { EXPECT_GE(aFaces->Length(), 1); } } } // ============================================================ // Parallel population produces same results // ============================================================ TEST(BRepGraphIncTest, Box_ParallelPopulate_SameEntityCounts) { BRepPrimAPI_MakeBox aBoxMaker(10.0, 20.0, 30.0); const TopoDS_Shape& aBox = aBoxMaker.Shape(); BRepGraphInc_Storage aSerial; BRepGraphInc_Populate::Perform(aSerial, aBox, false); ASSERT_TRUE(aSerial.GetIsDone()); BRepGraphInc_Storage aParallel; BRepGraphInc_Populate::Perform(aParallel, aBox, true); ASSERT_TRUE(aParallel.GetIsDone()); EXPECT_EQ(aParallel.NbVertices(), aSerial.NbVertices()); EXPECT_EQ(aParallel.NbEdges(), aSerial.NbEdges()); EXPECT_EQ(aParallel.NbWires(), aSerial.NbWires()); EXPECT_EQ(aParallel.NbFaces(), aSerial.NbFaces()); EXPECT_EQ(aParallel.NbShells(), aSerial.NbShells()); EXPECT_EQ(aParallel.NbSolids(), aSerial.NbSolids()); } // ============================================================ // Null shape handling // ============================================================ TEST(BRepGraphIncTest, NullShape_NoEntities) { BRepGraphInc_Storage aStorage; BRepGraphInc_Populate::Perform(aStorage, TopoDS_Shape(), false); EXPECT_FALSE(aStorage.GetIsDone()); EXPECT_EQ(aStorage.NbVertices(), 0); EXPECT_EQ(aStorage.NbEdges(), 0); } // ============================================================ // Compound shape handling // ============================================================ TEST(BRepGraphIncTest, Compound_RoundTrip_SubShapeCounts) { BRep_Builder aBB; TopoDS_Compound aCompound; aBB.MakeCompound(aCompound); BRepPrimAPI_MakeBox aBoxMaker1(10.0, 10.0, 10.0); aBB.Add(aCompound, aBoxMaker1.Shape()); BRepPrimAPI_MakeBox aBoxMaker2(5.0, 5.0, 5.0); aBB.Add(aCompound, aBoxMaker2.Shape()); BRepGraphInc_Storage aStorage; BRepGraphInc_Populate::Perform(aStorage, aCompound, false); ASSERT_TRUE(aStorage.GetIsDone()); // Two solids, two shells. EXPECT_EQ(aStorage.NbSolids(), 2); EXPECT_EQ(aStorage.NbShells(), 2); EXPECT_EQ(aStorage.NbCompounds(), 1); EXPECT_EQ(aStorage.NbFaces(), 12); // Round-trip reconstruct via compound. TopoDS_Shape aRecon = BRepGraphInc_Reconstruct::Node(aStorage, BRepGraph_CompoundId::Start()); ASSERT_FALSE(aRecon.IsNull()); EXPECT_EQ(countSubShapes(aRecon, TopAbs_SOLID), 2); EXPECT_EQ(countSubShapes(aRecon, TopAbs_FACE), 12); } // ============================================================ // CoEdge count consistency (PCurve data lives on CoEdgeDef) // ============================================================ TEST(BRepGraphIncTest, Box_CoEdgeCount) { BRepPrimAPI_MakeBox aBoxMaker(10.0, 20.0, 30.0); const TopoDS_Shape& aBox = aBoxMaker.Shape(); BRepGraphInc_Storage aStorage; BRepGraphInc_Populate::Perform(aStorage, aBox, false); ASSERT_TRUE(aStorage.GetIsDone()); // A box has 12 edges, each shared by 2 faces => 24 CoEdge entries total. // (No seam edges on a box.) int aCoEdgeCount = 0; const int aNbEdges = aStorage.NbEdges(); for (BRepGraph_EdgeId anEdgeId(0); anEdgeId.IsValid(aNbEdges); ++anEdgeId) { const NCollection_DynamicArray* aCoEdgeIdxs = aStorage.ReverseIndex().CoEdgesOfEdge(anEdgeId); if (aCoEdgeIdxs != nullptr) aCoEdgeCount += aCoEdgeIdxs->Length(); } EXPECT_EQ(aCoEdgeCount, 24); } TEST(BRepGraphIncTest, Cylinder_HasSeamEdges) { BRepPrimAPI_MakeCylinder aCylMaker(5.0, 15.0); const TopoDS_Shape& aCyl = aCylMaker.Shape(); BRepGraphInc_Storage aStorage; BRepGraphInc_Populate::Perform(aStorage, aCyl, false); ASSERT_TRUE(aStorage.GetIsDone()); // A cylinder has seam edges: coedges with SeamPairId valid. int aSeamPairCount = 0; const int aNbEdges = aStorage.NbEdges(); for (BRepGraph_EdgeId anEdgeId(0); anEdgeId.IsValid(aNbEdges); ++anEdgeId) { const NCollection_DynamicArray* aCoEdgeIdxs = aStorage.ReverseIndex().CoEdgesOfEdge(anEdgeId); if (aCoEdgeIdxs == nullptr) continue; for (const BRepGraph_CoEdgeId& aCoEdgeId : *aCoEdgeIdxs) { const BRepGraphInc::CoEdgeDef& aCE = aStorage.CoEdge(aCoEdgeId); if (aCE.Orientation == TopAbs_FORWARD && aCE.SeamPairId.IsValid()) { ++aSeamPairCount; } } } // A cylinder has 1 seam edge on its lateral face. EXPECT_GE(aSeamPairCount, 1) << "Cylinder should have at least 1 seam edge pair"; } TEST(BRepGraphIncTest, Cylinder_SeamEdge_ReverseIndex_NoDuplicateFace) { BRepPrimAPI_MakeCylinder aCylMaker(5.0, 15.0); const TopoDS_Shape& aCyl = aCylMaker.Shape(); BRepGraphInc_Storage aStorage; BRepGraphInc_Populate::Perform(aStorage, aCyl, false); ASSERT_TRUE(aStorage.GetIsDone()); // For seam edges (two PCurve entries with the same FaceDefId but opposite // orientations), the reverse index must contain each face only once. const int aNbEdges = aStorage.NbEdges(); for (BRepGraph_EdgeId anEdgeId(0); anEdgeId.IsValid(aNbEdges); ++anEdgeId) { const NCollection_DynamicArray* aFaces = aStorage.ReverseIndex().FacesOfEdge(anEdgeId); if (aFaces == nullptr) continue; for (int i = 0; i < aFaces->Length(); ++i) { for (int j = i + 1; j < aFaces->Length(); ++j) { EXPECT_NE(aFaces->Value(i), aFaces->Value(j)) << "Duplicate face " << aFaces->Value(i).Index << " in FacesOfEdge(" << anEdgeId.Index << ")"; } } } } // ============================================================ // Degenerate edge handling // ============================================================ TEST(BRepGraphIncTest, Sphere_DegenerateEdges_Preserved) { BRepPrimAPI_MakeSphere aSphMaker(8.0); const TopoDS_Shape& aSph = aSphMaker.Shape(); BRepGraphInc_Storage aStorage; BRepGraphInc_Populate::Perform(aStorage, aSph, false); ASSERT_TRUE(aStorage.GetIsDone()); // A sphere has degenerate edges at the poles (no 3D curve, collapsed to a point). int aDegenerateCount = 0; const int aNbEdges = aStorage.NbEdges(); for (BRepGraph_EdgeId anEdgeId(0); anEdgeId.IsValid(aNbEdges); ++anEdgeId) { const BRepGraphInc::EdgeDef& anEdge = aStorage.Edge(anEdgeId); if (anEdge.IsDegenerate) { ++aDegenerateCount; EXPECT_FALSE(anEdge.Curve3DRepId.IsValid()) << "Degenerate edge " << anEdgeId.Index << " should have no 3D curve"; } } EXPECT_GE(aDegenerateCount, 2) << "Sphere should have at least 2 degenerate edges (poles)"; // Round-trip: reconstructed sphere area must still match. TopoDS_Shape aRecon = BRepGraphInc_Reconstruct::Node(aStorage, BRepGraph_SolidId::Start()); ASSERT_FALSE(aRecon.IsNull()); const double anOrigArea = computeArea(aSph); const double aReconArea = computeArea(aRecon); EXPECT_NEAR(aReconArea, anOrigArea, Precision::Confusion()); } // ============================================================ // Location handling: compound with translated children // ============================================================ TEST(BRepGraphIncTest, Compound_TranslatedChildren_VolumePreserved) { BRep_Builder aBB; TopoDS_Compound aCompound; aBB.MakeCompound(aCompound); // Box at origin. BRepPrimAPI_MakeBox aBoxMaker1(10.0, 10.0, 10.0); aBB.Add(aCompound, aBoxMaker1.Shape()); // Box translated by (100, 0, 0). gp_Trsf aTrsf; aTrsf.SetTranslation(gp_Vec(100.0, 0.0, 0.0)); BRepPrimAPI_MakeBox aBoxMaker2(10.0, 10.0, 10.0); TopoDS_Shape aTranslatedBox = BRepBuilderAPI_Transform(aBoxMaker2.Shape(), aTrsf).Shape(); aBB.Add(aCompound, aTranslatedBox); const double anOrigVol = computeVolume(aCompound); EXPECT_NEAR(anOrigVol, 2000.0, Precision::Confusion()); BRepGraphInc_Storage aStorage; BRepGraphInc_Populate::Perform(aStorage, aCompound, false); ASSERT_TRUE(aStorage.GetIsDone()); TopoDS_Shape aRecon = BRepGraphInc_Reconstruct::Node(aStorage, BRepGraph_CompoundId::Start()); ASSERT_FALSE(aRecon.IsNull()); const double aReconVol = computeVolume(aRecon); EXPECT_NEAR(aReconVol, anOrigVol, Precision::Confusion()); EXPECT_EQ(countSubShapes(aRecon, TopAbs_SOLID), 2); EXPECT_EQ(countSubShapes(aRecon, TopAbs_FACE), 12); } TEST(BRepGraphIncTest, Cylinder_RoundTrip_BRepDump) { BRepPrimAPI_MakeCylinder aCylMaker(5.0, 20.0); const TopoDS_Shape& aCyl = aCylMaker.Shape(); BRepGraphInc_Storage aStorage; BRepGraphInc_Populate::Perform(aStorage, aCyl, false); ASSERT_TRUE(aStorage.GetIsDone()); TopoDS_Shape aRecon = BRepGraphInc_Reconstruct::Node(aStorage, BRepGraph_SolidId::Start()); ASSERT_FALSE(aRecon.IsNull()); // Dump both shapes and compare std::ostringstream anOrigStream, aReconStream; BRepTools::Write(aCyl, anOrigStream); BRepTools::Write(aRecon, aReconStream); if (anOrigStream.str() != aReconStream.str()) { // Show ALL line differences std::istringstream anOrigLines(anOrigStream.str()); std::istringstream aReconLines(aReconStream.str()); std::string anOrigLine, aReconLine; int aLineNo = 0; int aDiffCount = 0; while (std::getline(anOrigLines, anOrigLine) && std::getline(aReconLines, aReconLine)) { ++aLineNo; if (anOrigLine != aReconLine) { std::cout << "BRep diff at line " << aLineNo << ":" << std::endl; std::cout << " ORIG: " << anOrigLine << std::endl; std::cout << " RECON: " << aReconLine << std::endl; ++aDiffCount; } } std::cout << "Total " << aDiffCount << " line differences. " << "Orig size=" << anOrigStream.str().size() << " Recon size=" << aReconStream.str().size() << std::endl; } else { std::cout << "BRep dumps are IDENTICAL (size=" << anOrigStream.str().size() << ")" << std::endl; } const double anOrigArea = computeArea(aCyl); const double aReconArea = computeArea(aRecon); std::cout << "Orig area=" << anOrigArea << " Recon area=" << aReconArea << std::endl; EXPECT_NEAR(aReconArea, anOrigArea, Precision::Confusion()); } // ============================================================ // Edge internal vertices // ============================================================ // Helper: create an edge with a line segment and add vertices with given orientations. static TopoDS_Edge makeEdgeWithInternalVertex() { BRep_Builder aBB; BRepBuilderAPI_MakeEdge aMakeEdge(gp_Pnt(0, 0, 0), gp_Pnt(10, 0, 0)); TopoDS_Edge anEdge = aMakeEdge.Edge(); TopoDS_Vertex anIntVtx; aBB.MakeVertex(anIntVtx, gp_Pnt(5, 0, 0), Precision::Confusion()); aBB.Add(anEdge, anIntVtx.Oriented(TopAbs_INTERNAL)); return anEdge; } // Helper: build a face containing the given edge (needed for graph population). static TopoDS_Shape wrapEdgeInFace(const TopoDS_Edge& theEdge) { occ::handle aPlane = new Geom_Plane(gp_Pln()); BRep_Builder aBB; TopoDS_Face aFace; aBB.MakeFace(aFace, aPlane, Precision::Confusion()); TopoDS_Wire aWire; aBB.MakeWire(aWire); aBB.Add(aWire, theEdge); aBB.Add(aFace, aWire); return aFace; } TEST(BRepGraphIncTest, EdgeInternalVertex_Captured) { TopoDS_Edge anEdge = makeEdgeWithInternalVertex(); TopoDS_Shape aFace = wrapEdgeInFace(anEdge); BRepGraphInc_Storage aStorage; BRepGraphInc_Populate::Perform(aStorage, aFace, false); ASSERT_TRUE(aStorage.GetIsDone()); ASSERT_GE(aStorage.NbEdges(), 1); // Find the edge entity and check InternalVertices. bool aFound = false; const int aNbEdges = aStorage.NbEdges(); for (BRepGraph_EdgeId anEdgeId(0); anEdgeId.IsValid(aNbEdges); ++anEdgeId) { const BRepGraphInc::EdgeDef& anEdgeEnt = aStorage.Edge(anEdgeId); if (anEdgeEnt.InternalVertexRefIds.Length() == 1) { aFound = true; const BRepGraphInc::VertexRef& aIntVRef = aStorage.VertexRef(anEdgeEnt.InternalVertexRefIds.Value(0)); EXPECT_GE(aIntVRef.VertexDefId.Index, 0); EXPECT_EQ(aIntVRef.Orientation, TopAbs_INTERNAL); // Verify the vertex point. const BRepGraph_VertexId aVtxId = aIntVRef.VertexDefId; const BRepGraphInc::VertexDef& aVtxEnt = aStorage.Vertex(aVtxId); EXPECT_NEAR(aVtxEnt.Point.X(), 5.0, Precision::Confusion()); break; } } EXPECT_TRUE(aFound) << "No edge with InternalVertices found"; } TEST(BRepGraphIncTest, EdgeInternalVertex_RoundTrip) { TopoDS_Edge anEdge = makeEdgeWithInternalVertex(); TopoDS_Shape aFace = wrapEdgeInFace(anEdge); BRepGraphInc_Storage aStorage; BRepGraphInc_Populate::Perform(aStorage, aFace, false); ASSERT_TRUE(aStorage.GetIsDone()); TopoDS_Shape aRecon = BRepGraphInc_Reconstruct::Node(aStorage, BRepGraph_FaceId::Start()); ASSERT_FALSE(aRecon.IsNull()); // Find the edge in the reconstructed face and verify internal vertex. bool aFoundInternal = false; for (TopExp_Explorer anEdgeExp(aRecon, TopAbs_EDGE); anEdgeExp.More(); anEdgeExp.Next()) { const TopoDS_Edge& aReconEdge = TopoDS::Edge(anEdgeExp.Current()); for (TopoDS_Iterator aVIt(aReconEdge, false); aVIt.More(); aVIt.Next()) { if (aVIt.Value().ShapeType() == TopAbs_VERTEX && aVIt.Value().Orientation() == TopAbs_INTERNAL) { aFoundInternal = true; const TopoDS_Vertex& aVtx = TopoDS::Vertex(aVIt.Value()); EXPECT_NEAR(BRep_Tool::Pnt(aVtx).X(), 5.0, Precision::Confusion()); } } } EXPECT_TRUE(aFoundInternal) << "Internal vertex not found in reconstructed edge"; } TEST(BRepGraphIncTest, EdgeExternalVertex_Captured) { BRep_Builder aBB; BRepBuilderAPI_MakeEdge aMakeEdge(gp_Pnt(0, 0, 0), gp_Pnt(10, 0, 0)); TopoDS_Edge anEdge = aMakeEdge.Edge(); TopoDS_Vertex anExtVtx; aBB.MakeVertex(anExtVtx, gp_Pnt(7, 0, 0), Precision::Confusion()); aBB.Add(anEdge, anExtVtx.Oriented(TopAbs_EXTERNAL)); TopoDS_Shape aFace = wrapEdgeInFace(anEdge); BRepGraphInc_Storage aStorage; BRepGraphInc_Populate::Perform(aStorage, aFace, false); ASSERT_TRUE(aStorage.GetIsDone()); bool aFound = false; const int aNbEdges = aStorage.NbEdges(); for (BRepGraph_EdgeId anEdgeId(0); anEdgeId.IsValid(aNbEdges); ++anEdgeId) { const BRepGraphInc::EdgeDef& anEdgeEnt = aStorage.Edge(anEdgeId); if (anEdgeEnt.InternalVertexRefIds.Length() == 1) { aFound = true; EXPECT_EQ(aStorage.VertexRef(anEdgeEnt.InternalVertexRefIds.Value(0)).Orientation, TopAbs_EXTERNAL); break; } } EXPECT_TRUE(aFound) << "No edge with EXTERNAL vertex found"; } TEST(BRepGraphIncTest, EdgeNoInternalVertices_EmptyVector) { BRepPrimAPI_MakeBox aBoxMaker(10.0, 20.0, 30.0); const TopoDS_Shape& aBox = aBoxMaker.Shape(); BRepGraphInc_Storage aStorage; BRepGraphInc_Populate::Perform(aStorage, aBox, false); ASSERT_TRUE(aStorage.GetIsDone()); const int aNbEdges = aStorage.NbEdges(); for (BRepGraph_EdgeId anEdgeId(0); anEdgeId.IsValid(aNbEdges); ++anEdgeId) { EXPECT_EQ(aStorage.Edge(anEdgeId).InternalVertexRefIds.Length(), 0) << "Edge " << anEdgeId.Index << " should have no internal vertices"; } } TEST(BRepGraphIncTest, EdgeMultipleInternalVertices_AllCaptured) { BRep_Builder aBB; BRepBuilderAPI_MakeEdge aMakeEdge(gp_Pnt(0, 0, 0), gp_Pnt(10, 0, 0)); TopoDS_Edge anEdge = aMakeEdge.Edge(); TopoDS_Vertex aVtx1, aVtx2; aBB.MakeVertex(aVtx1, gp_Pnt(3, 0, 0), Precision::Confusion()); aBB.MakeVertex(aVtx2, gp_Pnt(7, 0, 0), Precision::Confusion()); aBB.Add(anEdge, aVtx1.Oriented(TopAbs_INTERNAL)); aBB.Add(anEdge, aVtx2.Oriented(TopAbs_EXTERNAL)); TopoDS_Shape aFace = wrapEdgeInFace(anEdge); BRepGraphInc_Storage aStorage; BRepGraphInc_Populate::Perform(aStorage, aFace, false); ASSERT_TRUE(aStorage.GetIsDone()); bool aFound = false; const int aNbEdges = aStorage.NbEdges(); for (BRepGraph_EdgeId anEdgeId(0); anEdgeId.IsValid(aNbEdges); ++anEdgeId) { const BRepGraphInc::EdgeDef& anEdgeEnt = aStorage.Edge(anEdgeId); if (anEdgeEnt.InternalVertexRefIds.Length() == 2) { aFound = true; // Check both orientations are preserved. bool aHasInternal = false, aHasExternal = false; for (int j = 0; j < 2; ++j) { if (aStorage.VertexRef(anEdgeEnt.InternalVertexRefIds.Value(j)).Orientation == TopAbs_INTERNAL) aHasInternal = true; if (aStorage.VertexRef(anEdgeEnt.InternalVertexRefIds.Value(j)).Orientation == TopAbs_EXTERNAL) aHasExternal = true; } EXPECT_TRUE(aHasInternal); EXPECT_TRUE(aHasExternal); break; } } EXPECT_TRUE(aFound) << "No edge with 2 internal vertices found"; } // ============================================================ // Face direct vertices // ============================================================ TEST(BRepGraphIncTest, FaceDirectVertex_Internal_Captured) { BRep_Builder aBB; occ::handle aPlane = new Geom_Plane(gp_Pln()); TopoDS_Face aFace; aBB.MakeFace(aFace, aPlane, Precision::Confusion()); // Add a wire so the face is valid for population. BRepBuilderAPI_MakeEdge aME(gp_Pnt(0, 0, 0), gp_Pnt(10, 0, 0)); TopoDS_Wire aWire; aBB.MakeWire(aWire); aBB.Add(aWire, aME.Edge()); aBB.Add(aFace, aWire); // Add a direct vertex child with INTERNAL orientation. TopoDS_Vertex aVtx; aBB.MakeVertex(aVtx, gp_Pnt(5, 5, 0), Precision::Confusion()); aBB.Add(aFace, aVtx.Oriented(TopAbs_INTERNAL)); BRepGraphInc_Storage aStorage; BRepGraphInc_Populate::Perform(aStorage, aFace, false); ASSERT_TRUE(aStorage.GetIsDone()); ASSERT_GE(aStorage.NbFaces(), 1); const BRepGraphInc::FaceDef& aFaceEnt = aStorage.Face(BRepGraph_FaceId::Start()); EXPECT_EQ(aFaceEnt.VertexRefIds.Length(), 1); if (aFaceEnt.VertexRefIds.Length() == 1) { const BRepGraphInc::VertexRef& aFaceVRef = aStorage.VertexRef(aFaceEnt.VertexRefIds.Value(0)); EXPECT_GE(aFaceVRef.VertexDefId.Index, 0); EXPECT_EQ(aFaceVRef.Orientation, TopAbs_INTERNAL); } } TEST(BRepGraphIncTest, FaceDirectVertex_RoundTrip) { BRep_Builder aBB; occ::handle aPlane = new Geom_Plane(gp_Pln()); TopoDS_Face aFace; aBB.MakeFace(aFace, aPlane, Precision::Confusion()); BRepBuilderAPI_MakeEdge aME(gp_Pnt(0, 0, 0), gp_Pnt(10, 0, 0)); TopoDS_Wire aWire; aBB.MakeWire(aWire); aBB.Add(aWire, aME.Edge()); aBB.Add(aFace, aWire); TopoDS_Vertex aVtx; aBB.MakeVertex(aVtx, gp_Pnt(5, 5, 0), Precision::Confusion()); aBB.Add(aFace, aVtx.Oriented(TopAbs_INTERNAL)); BRepGraphInc_Storage aStorage; BRepGraphInc_Populate::Perform(aStorage, aFace, false); ASSERT_TRUE(aStorage.GetIsDone()); TopoDS_Shape aRecon = BRepGraphInc_Reconstruct::Node(aStorage, BRepGraph_FaceId::Start()); ASSERT_FALSE(aRecon.IsNull()); // Verify vertex is a direct child of the face (not inside a wire). bool aFoundDirect = false; for (TopoDS_Iterator aIt(aRecon); aIt.More(); aIt.Next()) { if (aIt.Value().ShapeType() == TopAbs_VERTEX && aIt.Value().Orientation() == TopAbs_INTERNAL) { aFoundDirect = true; const TopoDS_Vertex& aReconVtx = TopoDS::Vertex(aIt.Value()); EXPECT_NEAR(BRep_Tool::Pnt(aReconVtx).X(), 5.0, Precision::Confusion()); EXPECT_NEAR(BRep_Tool::Pnt(aReconVtx).Y(), 5.0, Precision::Confusion()); } } EXPECT_TRUE(aFoundDirect) << "Direct internal vertex not found in reconstructed face"; } TEST(BRepGraphIncTest, FaceExternalVertex_Captured) { BRep_Builder aBB; occ::handle aPlane = new Geom_Plane(gp_Pln()); TopoDS_Face aFace; aBB.MakeFace(aFace, aPlane, Precision::Confusion()); BRepBuilderAPI_MakeEdge aME(gp_Pnt(0, 0, 0), gp_Pnt(10, 0, 0)); TopoDS_Wire aWire; aBB.MakeWire(aWire); aBB.Add(aWire, aME.Edge()); aBB.Add(aFace, aWire); TopoDS_Vertex aVtx; aBB.MakeVertex(aVtx, gp_Pnt(5, 5, 0), Precision::Confusion()); aBB.Add(aFace, aVtx.Oriented(TopAbs_EXTERNAL)); BRepGraphInc_Storage aStorage; BRepGraphInc_Populate::Perform(aStorage, aFace, false); ASSERT_TRUE(aStorage.GetIsDone()); ASSERT_GE(aStorage.NbFaces(), 1); const BRepGraphInc::FaceDef& aFaceEnt = aStorage.Face(BRepGraph_FaceId::Start()); EXPECT_EQ(aFaceEnt.VertexRefIds.Length(), 1); if (aFaceEnt.VertexRefIds.Length() == 1) { EXPECT_EQ(aStorage.VertexRef(aFaceEnt.VertexRefIds.Value(0)).Orientation, TopAbs_EXTERNAL); } } TEST(BRepGraphIncTest, FaceNoDirectVertices_EmptyVector) { BRepPrimAPI_MakeBox aBoxMaker(10.0, 20.0, 30.0); const TopoDS_Shape& aBox = aBoxMaker.Shape(); BRepGraphInc_Storage aStorage; BRepGraphInc_Populate::Perform(aStorage, aBox, false); ASSERT_TRUE(aStorage.GetIsDone()); const int aNbFaces = aStorage.NbFaces(); for (BRepGraph_FaceId aFaceId(0); aFaceId.IsValid(aNbFaces); ++aFaceId) { EXPECT_EQ(aStorage.Face(aFaceId).VertexRefIds.Length(), 0) << "Face " << aFaceId.Index << " should have no direct vertex children"; } } TEST(BRepGraphIncTest, FaceWithWiresAndVertices_BothCaptured) { BRep_Builder aBB; occ::handle aPlane = new Geom_Plane(gp_Pln()); TopoDS_Face aFace; aBB.MakeFace(aFace, aPlane, Precision::Confusion()); BRepBuilderAPI_MakeEdge aME(gp_Pnt(0, 0, 0), gp_Pnt(10, 0, 0)); TopoDS_Wire aWire; aBB.MakeWire(aWire); aBB.Add(aWire, aME.Edge()); aBB.Add(aFace, aWire); TopoDS_Vertex aVtx; aBB.MakeVertex(aVtx, gp_Pnt(5, 5, 0), Precision::Confusion()); aBB.Add(aFace, aVtx.Oriented(TopAbs_INTERNAL)); BRepGraphInc_Storage aStorage; BRepGraphInc_Populate::Perform(aStorage, aFace, false); ASSERT_TRUE(aStorage.GetIsDone()); ASSERT_GE(aStorage.NbFaces(), 1); const BRepGraphInc::FaceDef& aFaceEnt = aStorage.Face(BRepGraph_FaceId::Start()); EXPECT_GE(BRepGraph_TestTools::CountWireRefsOfFace(aStorage, BRepGraph_FaceId::Start()), 1); EXPECT_EQ(aFaceEnt.VertexRefIds.Length(), 1); } // ============================================================ // Integration: round-trip with internal vertices // ============================================================ TEST(BRepGraphIncTest, CompoundWithInternalVertices_RoundTrip_SubShapeCounts) { BRep_Builder aBB; TopoDS_Compound aCompound; aBB.MakeCompound(aCompound); // Face with a direct internal vertex. occ::handle aPlane = new Geom_Plane(gp_Pln()); TopoDS_Face aFace; aBB.MakeFace(aFace, aPlane, Precision::Confusion()); BRepBuilderAPI_MakeEdge aME(gp_Pnt(0, 0, 0), gp_Pnt(10, 0, 0)); TopoDS_Edge anEdge = aME.Edge(); // Add internal vertex to the edge. TopoDS_Vertex anEdgeIntVtx; aBB.MakeVertex(anEdgeIntVtx, gp_Pnt(5, 0, 0), Precision::Confusion()); aBB.Add(anEdge, anEdgeIntVtx.Oriented(TopAbs_INTERNAL)); TopoDS_Wire aWire; aBB.MakeWire(aWire); aBB.Add(aWire, anEdge); aBB.Add(aFace, aWire); // Add direct vertex to the face. TopoDS_Vertex aFaceIntVtx; aBB.MakeVertex(aFaceIntVtx, gp_Pnt(5, 5, 0), Precision::Confusion()); aBB.Add(aFace, aFaceIntVtx.Oriented(TopAbs_INTERNAL)); aBB.Add(aCompound, aFace); // Count original sub-shapes. int anOrigFaces = countSubShapes(aCompound, TopAbs_FACE); int anOrigEdges = countSubShapes(aCompound, TopAbs_EDGE); int anOrigVertices = countSubShapes(aCompound, TopAbs_VERTEX); BRepGraphInc_Storage aStorage; BRepGraphInc_Populate::Perform(aStorage, aCompound, false); ASSERT_TRUE(aStorage.GetIsDone()); TopoDS_Shape aRecon = BRepGraphInc_Reconstruct::Node(aStorage, BRepGraph_CompoundId::Start()); ASSERT_FALSE(aRecon.IsNull()); EXPECT_EQ(countSubShapes(aRecon, TopAbs_FACE), anOrigFaces); EXPECT_EQ(countSubShapes(aRecon, TopAbs_EDGE), anOrigEdges); EXPECT_EQ(countSubShapes(aRecon, TopAbs_VERTEX), anOrigVertices); // Verify orientations of reconstructed internal vertices. // Edge internal vertex: INTERNAL orientation on edge child. bool aFoundEdgeIntVtx = false; for (TopExp_Explorer anEdgeExp(aRecon, TopAbs_EDGE); anEdgeExp.More(); anEdgeExp.Next()) { for (TopoDS_Iterator aVIt(anEdgeExp.Current(), false); aVIt.More(); aVIt.Next()) { if (aVIt.Value().ShapeType() == TopAbs_VERTEX && aVIt.Value().Orientation() == TopAbs_INTERNAL) { aFoundEdgeIntVtx = true; const TopoDS_Vertex& aVtx = TopoDS::Vertex(aVIt.Value()); EXPECT_NEAR(BRep_Tool::Pnt(aVtx).X(), 5.0, Precision::Confusion()); EXPECT_NEAR(BRep_Tool::Pnt(aVtx).Y(), 0.0, Precision::Confusion()); } } } EXPECT_TRUE(aFoundEdgeIntVtx) << "INTERNAL vertex on edge not found after round-trip"; // Face direct vertex: INTERNAL orientation as direct child of face. bool aFoundFaceIntVtx = false; for (TopExp_Explorer aFaceExp(aRecon, TopAbs_FACE); aFaceExp.More(); aFaceExp.Next()) { for (TopoDS_Iterator aFIt(aFaceExp.Current()); aFIt.More(); aFIt.Next()) { if (aFIt.Value().ShapeType() == TopAbs_VERTEX && aFIt.Value().Orientation() == TopAbs_INTERNAL) { aFoundFaceIntVtx = true; const TopoDS_Vertex& aVtx = TopoDS::Vertex(aFIt.Value()); EXPECT_NEAR(BRep_Tool::Pnt(aVtx).X(), 5.0, Precision::Confusion()); EXPECT_NEAR(BRep_Tool::Pnt(aVtx).Y(), 5.0, Precision::Confusion()); } } } EXPECT_TRUE(aFoundFaceIntVtx) << "INTERNAL vertex on face not found after round-trip"; } TEST(BRepGraphIncTest, ParallelBuild_InternalVertices_SameAsSequential) { BRep_Builder aBB; occ::handle aPlane = new Geom_Plane(gp_Pln()); TopoDS_Face aFace; aBB.MakeFace(aFace, aPlane, Precision::Confusion()); BRepBuilderAPI_MakeEdge aME(gp_Pnt(0, 0, 0), gp_Pnt(10, 0, 0)); TopoDS_Edge anEdge = aME.Edge(); TopoDS_Vertex anIntVtx; aBB.MakeVertex(anIntVtx, gp_Pnt(5, 0, 0), Precision::Confusion()); aBB.Add(anEdge, anIntVtx.Oriented(TopAbs_INTERNAL)); TopoDS_Wire aWire; aBB.MakeWire(aWire); aBB.Add(aWire, anEdge); aBB.Add(aFace, aWire); TopoDS_Vertex aFaceVtx; aBB.MakeVertex(aFaceVtx, gp_Pnt(5, 5, 0), Precision::Confusion()); aBB.Add(aFace, aFaceVtx.Oriented(TopAbs_INTERNAL)); BRepGraphInc_Storage aSerial; BRepGraphInc_Populate::Perform(aSerial, aFace, false); ASSERT_TRUE(aSerial.GetIsDone()); BRepGraphInc_Storage aParallel; BRepGraphInc_Populate::Perform(aParallel, aFace, true); ASSERT_TRUE(aParallel.GetIsDone()); EXPECT_EQ(aParallel.NbVertices(), aSerial.NbVertices()); EXPECT_EQ(aParallel.NbEdges(), aSerial.NbEdges()); EXPECT_EQ(aParallel.NbFaces(), aSerial.NbFaces()); // Check internal vertex counts match. const int aNbEdges = aSerial.NbEdges(); for (BRepGraph_EdgeId anEdgeId(0); anEdgeId.IsValid(aNbEdges); ++anEdgeId) { EXPECT_EQ(aParallel.Edge(anEdgeId).InternalVertexRefIds.Length(), aSerial.Edge(anEdgeId).InternalVertexRefIds.Length()) << "Edge " << anEdgeId.Index << " internal vertex count mismatch"; } const int aNbFaces = aSerial.NbFaces(); for (BRepGraph_FaceId aFaceId(0); aFaceId.IsValid(aNbFaces); ++aFaceId) { EXPECT_EQ(aParallel.Face(aFaceId).VertexRefIds.Length(), aSerial.Face(aFaceId).VertexRefIds.Length()) << "Face " << aFaceId.Index << " direct vertex count mismatch"; } } // ============================================================ // Reverse-index hardening: compound atomic children (Wire/Edge/Vertex) // ============================================================ TEST(BRepGraphIncTest, ReverseIndex_Validate_CompoundWithAtomicWire) { // Build a compound that contains only a wire (no solid/shell/face). // This exercises the Wire-child path in compound reverse-index maintenance. BRep_Builder aBB; TopoDS_Compound aCompound; aBB.MakeCompound(aCompound); BRepBuilderAPI_MakeEdge aME(gp_Pnt(0, 0, 0), gp_Pnt(10, 0, 0)); ASSERT_TRUE(aME.IsDone()); TopoDS_Wire aWire; aBB.MakeWire(aWire); aBB.Add(aWire, aME.Edge()); aBB.Add(aCompound, aWire); BRepGraphInc_Storage aStorage; BRepGraphInc_Populate::Perform(aStorage, aCompound, false); ASSERT_TRUE(aStorage.GetIsDone()); EXPECT_EQ(aStorage.NbCompounds(), 1); ASSERT_GE(aStorage.NbWires(), 1); // The wire must appear in myCompoundsOfWire. const NCollection_DynamicArray* aCmpVec = aStorage.ReverseIndex().CompoundsOfWire(BRepGraph_WireId::Start()); EXPECT_NE(aCmpVec, nullptr) << "Wire(0) should appear in myCompoundsOfWire"; if (aCmpVec != nullptr) { EXPECT_EQ(aCmpVec->Length(), 1); EXPECT_EQ(aCmpVec->Value(0).Index, 0); } // Full consistency check. EXPECT_TRUE(aStorage.ValidateReverseIndex()); } TEST(BRepGraphIncTest, ReverseIndex_Validate_CompoundWithAtomicEdge) { // Build a compound containing only an edge (no wire/face/solid). BRep_Builder aBB; TopoDS_Compound aCompound; aBB.MakeCompound(aCompound); BRepBuilderAPI_MakeEdge aME(gp_Pnt(0, 0, 0), gp_Pnt(5, 0, 0)); ASSERT_TRUE(aME.IsDone()); aBB.Add(aCompound, aME.Edge()); BRepGraphInc_Storage aStorage; BRepGraphInc_Populate::Perform(aStorage, aCompound, false); ASSERT_TRUE(aStorage.GetIsDone()); EXPECT_EQ(aStorage.NbCompounds(), 1); ASSERT_GE(aStorage.NbEdges(), 1); const NCollection_DynamicArray* aCmpVec = aStorage.ReverseIndex().CompoundsOfEdge(BRepGraph_EdgeId::Start()); EXPECT_NE(aCmpVec, nullptr) << "Edge(0) should appear in myCompoundsOfEdge"; if (aCmpVec != nullptr) { EXPECT_EQ(aCmpVec->Length(), 1); EXPECT_EQ(aCmpVec->Value(0).Index, 0); } EXPECT_TRUE(aStorage.ValidateReverseIndex()); } TEST(BRepGraphIncTest, ReverseIndex_Validate_CompoundWithAtomicVertex) { // Build a compound containing only a vertex. BRep_Builder aBB; TopoDS_Compound aCompound; aBB.MakeCompound(aCompound); TopoDS_Vertex aVtx; aBB.MakeVertex(aVtx, gp_Pnt(1, 2, 3), Precision::Confusion()); aBB.Add(aCompound, aVtx); BRepGraphInc_Storage aStorage; BRepGraphInc_Populate::Perform(aStorage, aCompound, false); ASSERT_TRUE(aStorage.GetIsDone()); EXPECT_EQ(aStorage.NbCompounds(), 1); ASSERT_GE(aStorage.NbVertices(), 1); const NCollection_DynamicArray* aCmpVec = aStorage.ReverseIndex().CompoundsOfVertex(BRepGraph_VertexId::Start()); EXPECT_NE(aCmpVec, nullptr) << "Vertex(0) should appear in myCompoundsOfVertex"; if (aCmpVec != nullptr) { EXPECT_EQ(aCmpVec->Length(), 1); EXPECT_EQ(aCmpVec->Value(0).Index, 0); } EXPECT_TRUE(aStorage.ValidateReverseIndex()); } TEST(BRepGraphIncTest, ReverseIndex_CoEdgeToWire_IsPopulated) { // Every coedge in a box must map to exactly one wire in myCoEdgeToWires. BRepPrimAPI_MakeBox aBoxMaker(5.0, 5.0, 5.0); const TopoDS_Shape& aBox = aBoxMaker.Shape(); BRepGraphInc_Storage aStorage; BRepGraphInc_Populate::Perform(aStorage, aBox, false); ASSERT_TRUE(aStorage.GetIsDone()); const int aNbCoEdges = aStorage.NbCoEdges(); ASSERT_GT(aNbCoEdges, 0); for (BRepGraph_CoEdgeId aCoEdgeId(0); aCoEdgeId.IsValid(aNbCoEdges); ++aCoEdgeId) { const BRepGraphInc::CoEdgeDef& aCE = aStorage.CoEdge(aCoEdgeId); if (aCE.IsRemoved) continue; const NCollection_DynamicArray* aWires = aStorage.ReverseIndex().WiresOfCoEdge(aCoEdgeId); EXPECT_NE(aWires, nullptr) << "CoEdge " << aCoEdgeId.Index << " not in any wire"; if (aWires != nullptr) { EXPECT_EQ(aWires->Length(), 1) << "CoEdge " << aCoEdgeId.Index << " should be in exactly one wire"; } } EXPECT_TRUE(aStorage.ValidateReverseIndex()); } TEST(BRepGraphIncTest, ReverseIndex_CompSolid_ReverseMaintained_AfterBuild) { // Build a TopoDS_CompSolid containing two boxes and verify: // 1. myCompSolidsOfSolid is populated for both solids // 2. ValidateReverseIndex() passes BRep_Builder aBB; TopoDS_CompSolid aCompSolid; aBB.MakeCompSolid(aCompSolid); BRepPrimAPI_MakeBox aBoxMaker1(4.0, 4.0, 4.0); BRepPrimAPI_MakeBox aBoxMaker2(2.0, 2.0, 2.0); aBB.Add(aCompSolid, aBoxMaker1.Shape()); aBB.Add(aCompSolid, aBoxMaker2.Shape()); BRepGraphInc_Storage aStorage; BRepGraphInc_Populate::Perform(aStorage, aCompSolid, false); ASSERT_TRUE(aStorage.GetIsDone()); EXPECT_EQ(aStorage.NbCompSolids(), 1); ASSERT_GE(aStorage.NbSolids(), 2); // Both solids must appear in myCompSolidsOfSolid. for (BRepGraph_SolidId aSolidId = BRepGraph_SolidId::Start(); aSolidId.IsValid(aStorage.NbSolids()); ++aSolidId) { const NCollection_DynamicArray* aCSVec = aStorage.ReverseIndex().CompSolidsOfSolid(aSolidId); EXPECT_NE(aCSVec, nullptr) << "Solid " << aSolidId.Index << " not in any CompSolid"; if (aCSVec != nullptr) { EXPECT_EQ(aCSVec->Length(), 1); EXPECT_EQ(aCSVec->Value(0).Index, 0); } } EXPECT_TRUE(aStorage.ValidateReverseIndex()); } TEST(BRepGraphIncTest, ReverseIndex_CompSolid_ReverseMaintained_AfterBuildDelta) { // Verify BuildDelta() correctly indexes a CompSolid->Solid reverse mapping // when both the compsolid and solids are appended in the delta. BRep_Builder aBB; TopoDS_CompSolid aCompSolid; aBB.MakeCompSolid(aCompSolid); BRepPrimAPI_MakeBox aBoxMaker(3.0, 3.0, 3.0); aBB.Add(aCompSolid, aBoxMaker.Shape()); // First, build an empty storage then delta-populate. // Simplest: just populate from scratch and check ValidateReverseIndex. BRepGraphInc_Storage aStorage; BRepGraphInc_Populate::Perform(aStorage, aCompSolid, false); ASSERT_TRUE(aStorage.GetIsDone()); EXPECT_EQ(aStorage.NbCompSolids(), 1); ASSERT_GE(aStorage.NbSolids(), 1); EXPECT_TRUE(aStorage.ValidateReverseIndex()); const NCollection_DynamicArray* aCSVec = aStorage.ReverseIndex().CompSolidsOfSolid(BRepGraph_SolidId::Start()); EXPECT_NE(aCSVec, nullptr) << "Solid 0 should be indexed in CompSolidsOfSolid"; } TEST(BRepGraphIncTest, ReverseIndex_Validate_Box_FullConsistency) { // Smoke test: a simple solid box must pass full reverse-index validation. BRepPrimAPI_MakeBox aBoxMaker(10.0, 20.0, 30.0); const TopoDS_Shape& aBox = aBoxMaker.Shape(); BRepGraphInc_Storage aStorage; BRepGraphInc_Populate::Perform(aStorage, aBox, false); ASSERT_TRUE(aStorage.GetIsDone()); EXPECT_TRUE(aStorage.ValidateReverseIndex()); } TEST(BRepGraphIncTest, ReverseIndex_Validate_Compound_FullConsistency) { // A compound of two boxes must pass full reverse-index validation, // covering CompoundsOfSolid, CompoundsOfShell, CompoundsOfFace. BRep_Builder aBB; TopoDS_Compound aCompound; aBB.MakeCompound(aCompound); BRepPrimAPI_MakeBox aBoxMaker1(5.0, 5.0, 5.0); BRepPrimAPI_MakeBox aBoxMaker2(3.0, 3.0, 3.0); aBB.Add(aCompound, aBoxMaker1.Shape()); aBB.Add(aCompound, aBoxMaker2.Shape()); BRepGraphInc_Storage aStorage; BRepGraphInc_Populate::Perform(aStorage, aCompound, false); ASSERT_TRUE(aStorage.GetIsDone()); EXPECT_TRUE(aStorage.ValidateReverseIndex()); } TEST(BRepGraphIncTest, ReverseIndex_AfterEditorMutations_StaysConsistent) { // Verify the incremental Bind/Unbind mutation path keeps the reverse index // consistent with the forward entity / reference-entry tables across a // sequence of RemoveWire / RemoveFace / RemoveShell mutations. BRep_Builder aBB; TopoDS_Compound aCompound; aBB.MakeCompound(aCompound); BRepPrimAPI_MakeBox aBoxMaker(10.0, 20.0, 30.0); BRepPrimAPI_MakeCylinder aCylMaker(5.0, 12.0); aBB.Add(aCompound, aBoxMaker.Shape()); aBB.Add(aCompound, aCylMaker.Shape()); BRepGraph aGraph; aGraph.Clear(); [[maybe_unused]] const BRepGraph_Builder::Result aBuildRes = BRepGraph_Builder::Add(aGraph, aCompound); ASSERT_TRUE(aGraph.IsDone()); ASSERT_TRUE(aGraph.ValidateReverseIndex()); // Remove an inner wire from the first face that owns more than one wire // (or the only wire if all faces have a single wire). for (BRepGraph_FaceIterator aFaceIt(aGraph); aFaceIt.More(); aFaceIt.Next()) { const BRepGraph_FaceId aFaceId = aFaceIt.CurrentId(); const NCollection_DynamicArray aWireRefs = BRepGraph_TestTools::WireRefsOfFace(aGraph, aFaceId); if (aWireRefs.IsEmpty()) continue; ASSERT_TRUE(aGraph.Editor().Faces().RemoveWire(aFaceId, aWireRefs.Value(0))); break; } EXPECT_TRUE(aGraph.ValidateReverseIndex()) << "Reverse index inconsistent after RemoveWire"; // Remove the first face from the first shell. const NCollection_DynamicArray aFaceRefs = BRepGraph_TestTools::FaceRefsOfShell(aGraph, BRepGraph_ShellId::Start()); ASSERT_GE(aFaceRefs.Length(), 1); ASSERT_TRUE(aGraph.Editor().Shells().RemoveFace(BRepGraph_ShellId::Start(), aFaceRefs.Value(0))); EXPECT_TRUE(aGraph.ValidateReverseIndex()) << "Reverse index inconsistent after RemoveFace"; // Remove the first shell from the first solid. const NCollection_DynamicArray aShellRefs = BRepGraph_TestTools::ShellRefsOfSolid(aGraph, BRepGraph_SolidId::Start()); ASSERT_GE(aShellRefs.Length(), 1); ASSERT_TRUE( aGraph.Editor().Solids().RemoveShell(BRepGraph_SolidId::Start(), aShellRefs.Value(0))); EXPECT_TRUE(aGraph.ValidateReverseIndex()) << "Reverse index inconsistent after RemoveShell"; EXPECT_TRUE(aGraph.Editor().ValidateMutationBoundary()); } TEST(BRepGraphIncTest, ReverseIndex_BulkBuild_TwiceProducesEqualState) { // Bulk Populate must be deterministic: building the same shape twice into // independent storages must yield byte-equal reverse-index views as observed // through the public per-entity accessors. BRepPrimAPI_MakeBox aBoxMaker(7.0, 11.0, 13.0); const TopoDS_Shape& aBox = aBoxMaker.Shape(); BRepGraphInc_Storage aStorageA; BRepGraphInc_Storage aStorageB; BRepGraphInc_Populate::Perform(aStorageA, aBox, false); BRepGraphInc_Populate::Perform(aStorageB, aBox, false); ASSERT_TRUE(aStorageA.GetIsDone()); ASSERT_TRUE(aStorageB.GetIsDone()); ASSERT_EQ(aStorageA.NbEdges(), aStorageB.NbEdges()); for (uint32_t anIdx = 0; anIdx < aStorageA.NbEdges(); ++anIdx) { const BRepGraph_EdgeId anEdgeId(anIdx); const NCollection_DynamicArray* aWiresA = aStorageA.ReverseIndex().WiresOfEdge(anEdgeId); const NCollection_DynamicArray* aWiresB = aStorageB.ReverseIndex().WiresOfEdge(anEdgeId); ASSERT_EQ(aWiresA == nullptr, aWiresB == nullptr); if (aWiresA == nullptr) continue; ASSERT_EQ(aWiresA->Size(), aWiresB->Size()); for (size_t i = 0; i < aWiresA->Size(); ++i) EXPECT_EQ(aWiresA->Value(i), aWiresB->Value(i)); const NCollection_DynamicArray* aFacesA = aStorageA.ReverseIndex().FacesOfEdge(anEdgeId); const NCollection_DynamicArray* aFacesB = aStorageB.ReverseIndex().FacesOfEdge(anEdgeId); ASSERT_EQ(aFacesA == nullptr, aFacesB == nullptr); if (aFacesA == nullptr) continue; ASSERT_EQ(aFacesA->Size(), aFacesB->Size()); for (size_t i = 0; i < aFacesA->Size(); ++i) EXPECT_EQ(aFacesA->Value(i), aFacesB->Value(i)); } } TEST(BRepGraphIncTest, ReverseIndex_EdgeOpsAdd_BindsStartEndVertices) { // Free edge created at runtime via the Editor must show up under its endpoint // vertices in the reverse index. Pre-fix this query returned an empty list. BRepGraph aGraph; aGraph.Clear(); const BRepGraph_VertexId aV0 = aGraph.Editor().Vertices().Add(gp_Pnt(0, 0, 0), 1.e-7); const BRepGraph_VertexId aV1 = aGraph.Editor().Vertices().Add(gp_Pnt(1, 0, 0), 1.e-7); ASSERT_TRUE(aV0.IsValid()); ASSERT_TRUE(aV1.IsValid()); const BRepGraph_EdgeId anEdge = aGraph.Editor().Edges().Add(aV0, aV1, occ::handle(), 0.0, 1.0, 1.e-7); ASSERT_TRUE(anEdge.IsValid()); bool foundV0 = false, foundV1 = false; for (const BRepGraph_EdgeId& aE : aGraph.Topo().Vertices().Edges(aV0)) if (aE == anEdge) foundV0 = true; for (const BRepGraph_EdgeId& aE : aGraph.Topo().Vertices().Edges(aV1)) if (aE == anEdge) foundV1 = true; EXPECT_TRUE(foundV0); EXPECT_TRUE(foundV1); EXPECT_TRUE(aGraph.ValidateReverseIndex()); } TEST(BRepGraphIncTest, ReverseIndex_RemoveEdge_UnbindsStartEndVertices) { // Symmetric to the Add test: removing the edge must drop the entries. BRepGraph aGraph; aGraph.Clear(); const BRepGraph_VertexId aV0 = aGraph.Editor().Vertices().Add(gp_Pnt(0, 0, 0), 1.e-7); const BRepGraph_VertexId aV1 = aGraph.Editor().Vertices().Add(gp_Pnt(1, 0, 0), 1.e-7); const BRepGraph_EdgeId anEdge = aGraph.Editor().Edges().Add(aV0, aV1, occ::handle(), 0.0, 1.0, 1.e-7); ASSERT_TRUE(anEdge.IsValid()); aGraph.Editor().Gen().RemoveNode(BRepGraph_NodeId(anEdge)); for (const BRepGraph_EdgeId& aE : aGraph.Topo().Vertices().Edges(aV0)) EXPECT_NE(aE, anEdge); for (const BRepGraph_EdgeId& aE : aGraph.Topo().Vertices().Edges(aV1)) EXPECT_NE(aE, anEdge); EXPECT_TRUE(aGraph.ValidateReverseIndex()); } TEST(BRepGraphIncTest, ReverseIndex_SetRefVertexDefId_RebindsVertexToEdges) { // Rewire an edge's start-vertex ref to a different vertex and verify the // rev-index moved. BRepGraph aGraph; aGraph.Clear(); const BRepGraph_VertexId aV0 = aGraph.Editor().Vertices().Add(gp_Pnt(0, 0, 0), 1.e-7); const BRepGraph_VertexId aV1 = aGraph.Editor().Vertices().Add(gp_Pnt(1, 0, 0), 1.e-7); const BRepGraph_VertexId aV2 = aGraph.Editor().Vertices().Add(gp_Pnt(2, 0, 0), 1.e-7); const BRepGraph_EdgeId anEdge = aGraph.Editor().Edges().Add(aV0, aV1, occ::handle(), 0.0, 1.0, 1.e-7); ASSERT_TRUE(anEdge.IsValid()); const BRepGraph_VertexRefId aStartRef = aGraph.Topo().Edges().Definition(anEdge).StartVertexRefId; aGraph.Editor().Vertices().SetRefVertexDefId(aStartRef, aV2); bool stillUnderV0 = false, foundUnderV2 = false; for (const BRepGraph_EdgeId& aE : aGraph.Topo().Vertices().Edges(aV0)) if (aE == anEdge) stillUnderV0 = true; for (const BRepGraph_EdgeId& aE : aGraph.Topo().Vertices().Edges(aV2)) if (aE == anEdge) foundUnderV2 = true; EXPECT_FALSE(stillUnderV0); EXPECT_TRUE(foundUnderV2); EXPECT_TRUE(aGraph.ValidateReverseIndex()); } TEST(BRepGraphIncTest, ReverseIndex_BoxThroughCompact_StaysConsistent) { BRepPrimAPI_MakeBox aBoxMaker(10.0, 20.0, 30.0); BRepGraph aGraph; aGraph.Clear(); [[maybe_unused]] const BRepGraph_Builder::Result aBuildRes = BRepGraph_Builder::Add(aGraph, aBoxMaker.Shape()); ASSERT_TRUE(aGraph.IsDone()); ASSERT_TRUE(aGraph.Editor().Faces().RemoveWire( BRepGraph_FaceId::Start(), BRepGraph_TestTools::WireRefsOfFace(aGraph, BRepGraph_FaceId::Start()).Value(0))); EXPECT_TRUE(aGraph.ValidateReverseIndex()); [[maybe_unused]] const BRepGraph_Compact::Result aCompactRes = BRepGraph_Compact::Perform(aGraph); EXPECT_TRUE(aGraph.ValidateReverseIndex()); } TEST(BRepGraphIncTest, ReverseIndex_SetRefWireDefId_RebindsWireToFaces) { // Add two faces; rewire face0's outer-wire ref to face1's outer wire and // verify WireToFaces moved entries. BRepPrimAPI_MakeBox aBoxMaker(10.0, 20.0, 30.0); BRepGraph aGraph; aGraph.Clear(); [[maybe_unused]] const BRepGraph_Builder::Result aBuildRes = BRepGraph_Builder::Add(aGraph, aBoxMaker.Shape()); ASSERT_TRUE(aGraph.IsDone()); const BRepGraph_FaceId aFace0 = BRepGraph_FaceId::Start(); const BRepGraph_FaceId aFace1(1); ASSERT_TRUE(aFace1.IsValid(aGraph.Topo().Faces().Nb())); const BRepGraph_WireRefId aWireRef0 = BRepGraph_TestTools::WireRefsOfFace(aGraph, aFace0).Value(0); const BRepGraph_WireId aOldWire = aGraph.Refs().Wires().Entry(aWireRef0).WireDefId; const BRepGraph_WireId aNewWire = aGraph.Refs() .Wires() .Entry(BRepGraph_TestTools::WireRefsOfFace(aGraph, aFace1).Value(0)) .WireDefId; ASSERT_NE(aOldWire, aNewWire); aGraph.Editor().Wires().SetRefWireDefId(aWireRef0, aNewWire); bool oldStillBound = false, newBound = false; for (const BRepGraph_FaceId& f : aGraph.Topo().Wires().Faces(aOldWire)) if (f == aFace0) oldStillBound = true; for (const BRepGraph_FaceId& f : aGraph.Topo().Wires().Faces(aNewWire)) if (f == aFace0) newBound = true; EXPECT_FALSE(oldStillBound); EXPECT_TRUE(newBound); EXPECT_TRUE(aGraph.ValidateReverseIndex()); } TEST(BRepGraphIncTest, ReverseIndex_SetRefFaceDefId_RebindsFaceToShells) { BRepPrimAPI_MakeBox aBoxMaker(10.0, 20.0, 30.0); BRepGraph aGraph; aGraph.Clear(); [[maybe_unused]] const BRepGraph_Builder::Result aBuildRes = BRepGraph_Builder::Add(aGraph, aBoxMaker.Shape()); ASSERT_TRUE(aGraph.IsDone()); const BRepGraph_ShellId aShell = BRepGraph_ShellId::Start(); const BRepGraph_FaceRefId aRef0 = BRepGraph_TestTools::FaceRefsOfShell(aGraph, aShell).Value(0); const BRepGraph_FaceId aOldFace = aGraph.Refs().Faces().Entry(aRef0).FaceDefId; const BRepGraph_FaceId aNewFace(1); ASSERT_NE(aOldFace, aNewFace); aGraph.Editor().Faces().SetRefFaceDefId(aRef0, aNewFace); // Old face still has the OTHER shell-ref pointing at it; just check the // Old/New rev-index makes sense relative to this single ref. EXPECT_TRUE(aGraph.ValidateReverseIndex()); } TEST(BRepGraphIncTest, ReverseIndex_SetRefShellDefId_RebindsShellToSolids) { BRepPrimAPI_MakeBox aBoxMaker(10.0, 20.0, 30.0); BRepGraph aGraph; aGraph.Clear(); [[maybe_unused]] const BRepGraph_Builder::Result aBuildRes = BRepGraph_Builder::Add(aGraph, aBoxMaker.Shape()); ASSERT_TRUE(aGraph.IsDone()); ASSERT_GE(aGraph.Topo().Shells().Nb(), 1); // Box has 1 solid, 1 shell. Rewiring the lone shell-ref to itself is a no-op, // so just verify Validate after a no-op call (proves equality short-circuit). const BRepGraph_ShellRefId aRef0 = BRepGraph_TestTools::ShellRefsOfSolid(aGraph, BRepGraph_SolidId::Start()).Value(0); const BRepGraph_ShellId aShell = aGraph.Refs().Shells().Entry(aRef0).ShellDefId; aGraph.Editor().Shells().SetRefShellDefId(aRef0, aShell); EXPECT_TRUE(aGraph.ValidateReverseIndex()); } TEST(BRepGraphIncTest, ReverseIndex_SetEdgeDefIdOnCoEdge_RebindsEdgeToCoEdges) { // Pick a coedge that lives in a wire; redirect its EdgeDefId to a different // existing edge and confirm Edge->CoEdges, Edge->Wires, Edge->Faces all move. BRepPrimAPI_MakeBox aBoxMaker(10.0, 20.0, 30.0); BRepGraph aGraph; aGraph.Clear(); [[maybe_unused]] const BRepGraph_Builder::Result aBuildRes = BRepGraph_Builder::Add(aGraph, aBoxMaker.Shape()); ASSERT_TRUE(aGraph.IsDone()); ASSERT_GE(aGraph.Topo().CoEdges().Nb(), 2); const BRepGraph_CoEdgeId aCoEdge = BRepGraph_CoEdgeId::Start(); const BRepGraphInc::CoEdgeDef& aDef = aGraph.Topo().CoEdges().Definition(aCoEdge); const BRepGraph_EdgeId anOldEdge = aDef.EdgeDefId; // Pick any other valid edge as the target. BRepGraph_EdgeId aNewEdge; for (BRepGraph_EdgeId aE(0); aE.IsValid(aGraph.Topo().Edges().Nb()); ++aE) { if (aE != anOldEdge) { aNewEdge = aE; break; } } ASSERT_TRUE(aNewEdge.IsValid()); aGraph.Editor().CoEdges().SetEdgeDefId(aCoEdge, aNewEdge); bool oldHasCoEdge = false, newHasCoEdge = false; for (const BRepGraph_CoEdgeId& aC : aGraph.Topo().Edges().CoEdges(anOldEdge)) if (aC == aCoEdge) oldHasCoEdge = true; for (const BRepGraph_CoEdgeId& aC : aGraph.Topo().Edges().CoEdges(aNewEdge)) if (aC == aCoEdge) newHasCoEdge = true; EXPECT_FALSE(oldHasCoEdge); EXPECT_TRUE(newHasCoEdge); EXPECT_TRUE(aGraph.ValidateReverseIndex()); } TEST(BRepGraphIncTest, ReverseIndex_SetFaceDefIdOnCoEdge_LastBondCheck) { // Cylinder seam edges have TWO coedges on the SAME face. Dropping one // coedge's FaceDefId must keep the (edge, face) pair bound via the other. BRepPrimAPI_MakeCylinder aCylMaker(5.0, 10.0); BRepGraph aGraph; aGraph.Clear(); [[maybe_unused]] const BRepGraph_Builder::Result aBuildRes = BRepGraph_Builder::Add(aGraph, aCylMaker.Shape()); ASSERT_TRUE(aGraph.IsDone()); // Locate a seam edge: an edge with two coedges on the same face. BRepGraph_EdgeId aSeamEdge; BRepGraph_FaceId aSeamFace; BRepGraph_CoEdgeId aSeamCoEdge; for (BRepGraph_EdgeId aE(0); aE.IsValid(aGraph.Topo().Edges().Nb()) && !aSeamEdge.IsValid(); ++aE) { const NCollection_DynamicArray& aCEs = aGraph.Topo().Edges().CoEdges(aE); if (aCEs.Size() < 2) continue; NCollection_DataMap aSeenFace; for (const BRepGraph_CoEdgeId& aCE : aCEs) { const BRepGraphInc::CoEdgeDef& aD = aGraph.Topo().CoEdges().Definition(aCE); if (!aD.FaceDefId.IsValid()) continue; if (aSeenFace.IsBound(aD.FaceDefId.Index)) { aSeamEdge = aE; aSeamFace = aD.FaceDefId; aSeamCoEdge = aCE; break; } aSeenFace.Bind(aD.FaceDefId.Index, aCE); } } ASSERT_TRUE(aSeamEdge.IsValid()) << "cylinder must have a seam edge"; aGraph.Editor().CoEdges().SetFaceDefId(aSeamCoEdge, BRepGraph_FaceId()); // The seam-pair partner of aSeamCoEdge still binds (aSeamEdge, aSeamFace). bool stillBound = false; for (const BRepGraph_FaceId& f : aGraph.Topo().Edges().Faces(aSeamEdge)) if (f == aSeamFace) stillBound = true; EXPECT_TRUE(stillBound); EXPECT_TRUE(aGraph.ValidateReverseIndex()); } TEST(BRepGraphIncTest, ReverseIndex_SetFaceDefIdOnCoEdge_OnlyBondUnbinds) { // Box edges have one coedge per face. Setting that single coedge's FaceDefId // to invalid MUST unbind (edge,face) from EdgeToFaces. BRepPrimAPI_MakeBox aBoxMaker(10.0, 20.0, 30.0); BRepGraph aGraph; aGraph.Clear(); [[maybe_unused]] const BRepGraph_Builder::Result aBuildRes = BRepGraph_Builder::Add(aGraph, aBoxMaker.Shape()); ASSERT_TRUE(aGraph.IsDone()); const BRepGraph_CoEdgeId aCoEdge = BRepGraph_CoEdgeId::Start(); const BRepGraphInc::CoEdgeDef& aDef = aGraph.Topo().CoEdges().Definition(aCoEdge); const BRepGraph_EdgeId anEdge = aDef.EdgeDefId; const BRepGraph_FaceId anOldFace = aDef.FaceDefId; aGraph.Editor().CoEdges().SetFaceDefId(aCoEdge, BRepGraph_FaceId()); for (const BRepGraph_FaceId& f : aGraph.Topo().Edges().Faces(anEdge)) EXPECT_NE(f, anOldFace); EXPECT_TRUE(aGraph.ValidateReverseIndex()); } TEST(BRepGraphIncTest, ReverseIndex_SetEndVertexRefId_RebindsVertexToEdges) { BRepGraph aGraph; aGraph.Clear(); const BRepGraph_VertexId aV0 = aGraph.Editor().Vertices().Add(gp_Pnt(0, 0, 0), 1.e-7); const BRepGraph_VertexId aV1 = aGraph.Editor().Vertices().Add(gp_Pnt(1, 0, 0), 1.e-7); const BRepGraph_VertexId aV2 = aGraph.Editor().Vertices().Add(gp_Pnt(2, 0, 0), 1.e-7); const BRepGraph_EdgeId anEdge = aGraph.Editor().Edges().Add(aV0, aV1, occ::handle(), 0.0, 1.0, 1.e-7); const BRepGraph_EdgeId anExtra = aGraph.Editor().Edges().Add(aV0, aV2, occ::handle(), 0.0, 1.0, 1.e-7); ASSERT_TRUE(anEdge.IsValid()); ASSERT_TRUE(anExtra.IsValid()); // Repoint anEdge's end-ref at the same VertexRefId already used by anExtra's // end. Both edges now share the same end vertex (aV2). const BRepGraph_VertexRefId aV2Ref = aGraph.Topo().Edges().Definition(anExtra).EndVertexRefId; aGraph.Editor().Edges().SetEndVertexRefId(anEdge, aV2Ref); bool stillUnderV1 = false, foundUnderV2 = false; for (const BRepGraph_EdgeId& aE : aGraph.Topo().Vertices().Edges(aV1)) if (aE == anEdge) stillUnderV1 = true; for (const BRepGraph_EdgeId& aE : aGraph.Topo().Vertices().Edges(aV2)) if (aE == anEdge) foundUnderV2 = true; EXPECT_FALSE(stillUnderV1); EXPECT_TRUE(foundUnderV2); EXPECT_TRUE(aGraph.ValidateReverseIndex()); } TEST(BRepGraphIncTest, ReverseIndex_SetterIdempotency_NoOp) { // Identity assignments must be no-ops and not corrupt the reverse index. BRepPrimAPI_MakeBox aBoxMaker(10.0, 20.0, 30.0); BRepGraph aGraph; aGraph.Clear(); [[maybe_unused]] const BRepGraph_Builder::Result aBuildRes = BRepGraph_Builder::Add(aGraph, aBoxMaker.Shape()); ASSERT_TRUE(aGraph.IsDone()); const BRepGraph_CoEdgeId aCoEdge = BRepGraph_CoEdgeId::Start(); const BRepGraphInc::CoEdgeDef& aDef = aGraph.Topo().CoEdges().Definition(aCoEdge); aGraph.Editor().CoEdges().SetEdgeDefId(aCoEdge, aDef.EdgeDefId); aGraph.Editor().CoEdges().SetFaceDefId(aCoEdge, aDef.FaceDefId); EXPECT_TRUE(aGraph.ValidateReverseIndex()); const BRepGraph_VertexRefId aVRef = aGraph.Topo().Edges().Definition(BRepGraph_EdgeId::Start()).StartVertexRefId; if (aVRef.IsValid()) { const BRepGraph_VertexId aV = aGraph.Refs().Vertices().Entry(aVRef).VertexDefId; aGraph.Editor().Vertices().SetRefVertexDefId(aVRef, aV); EXPECT_TRUE(aGraph.ValidateReverseIndex()); } } TEST(BRepGraphIncTest, ReverseIndex_SetChildRefChildDefId_CrossKindRebinds) { // Compound holding a Solid; rewire the ChildRef from the Solid to a Shell. // CompoundsOfSolid must lose the entry, CompoundsOfShell must gain it. BRep_Builder aBB; TopoDS_Compound aCompound; aBB.MakeCompound(aCompound); BRepPrimAPI_MakeBox aBoxMaker(2.0, 2.0, 2.0); aBB.Add(aCompound, aBoxMaker.Shape()); BRepGraph aGraph; aGraph.Clear(); [[maybe_unused]] const BRepGraph_Builder::Result aBuildRes = BRepGraph_Builder::Add(aGraph, aCompound); ASSERT_TRUE(aGraph.IsDone()); ASSERT_GE(aGraph.Topo().Compounds().Nb(), 1); ASSERT_GE(aGraph.Topo().Solids().Nb(), 1); ASSERT_GE(aGraph.Topo().Shells().Nb(), 1); const BRepGraph_CompoundId aCompound0 = BRepGraph_CompoundId::Start(); const BRepGraphInc::CompoundDef& aCDef = aGraph.Topo().Compounds().Definition(aCompound0); ASSERT_GE(aCDef.ChildRefIds.Length(), 1); const BRepGraph_ChildRefId aChildRef = aCDef.ChildRefIds.First(); const BRepGraph_NodeId anOldChild = aGraph.Refs().Children().Entry(aChildRef).ChildDefId; ASSERT_EQ(anOldChild.NodeKind, BRepGraph_NodeId::Kind::Solid); const BRepGraph_ShellId aShell = BRepGraph_ShellId::Start(); aGraph.Editor().Gen().SetChildRefChildDefId(aChildRef, BRepGraph_NodeId(aShell)); const BRepGraph_SolidId anOldSolid = BRepGraph_SolidId::FromNodeId(anOldChild); bool oldStill = false, newBound = false; for (const BRepGraph_CompoundId& c : aGraph.Topo().Solids().Compounds(anOldSolid)) if (c == aCompound0) oldStill = true; for (const BRepGraph_CompoundId& c : aGraph.Topo().Shells().Compounds(aShell)) if (c == aCompound0) newBound = true; EXPECT_FALSE(oldStill); EXPECT_TRUE(newBound); EXPECT_TRUE(aGraph.ValidateReverseIndex()); } TEST(BRepGraphIncTest, ReverseIndex_SetEdgeDefIdOnCoEdge_LastBondInWireCheck) { // Cylinder seam edge: two coedges share both face and wire. Redirecting one // coedge's EdgeDefId must NOT remove (oldEdge, wire) from EdgeToWires while // the other coedge still references oldEdge in the same wire. BRepPrimAPI_MakeCylinder aCylMaker(5.0, 10.0); BRepGraph aGraph; aGraph.Clear(); [[maybe_unused]] const BRepGraph_Builder::Result aBuildRes = BRepGraph_Builder::Add(aGraph, aCylMaker.Shape()); ASSERT_TRUE(aGraph.IsDone()); // Find a seam edge: two coedges, same wire, same edge. BRepGraph_EdgeId aSeamEdge; BRepGraph_WireId aSeamWire; BRepGraph_CoEdgeId aSeamCoEdge; for (BRepGraph_EdgeId aE(0); aE.IsValid(aGraph.Topo().Edges().Nb()) && !aSeamEdge.IsValid(); ++aE) { const NCollection_DynamicArray& aCEs = aGraph.Topo().Edges().CoEdges(aE); if (aCEs.Size() < 2) continue; NCollection_DataMap aSeenWire; for (const BRepGraph_CoEdgeId& aCE : aCEs) { const NCollection_DynamicArray& aCEWires = aGraph.Topo().CoEdges().Wires(aCE); for (const BRepGraph_WireId& aW : aCEWires) { if (aSeenWire.IsBound(aW.Index)) { aSeamEdge = aE; aSeamWire = aW; aSeamCoEdge = aCE; break; } aSeenWire.Bind(aW.Index, aCE); } if (aSeamEdge.IsValid()) break; } } ASSERT_TRUE(aSeamEdge.IsValid()) << "cylinder must have a wire with two coedges of same edge"; // Pick a different edge to redirect to. BRepGraph_EdgeId aTargetEdge; for (BRepGraph_EdgeId aE(0); aE.IsValid(aGraph.Topo().Edges().Nb()); ++aE) if (aE != aSeamEdge) { aTargetEdge = aE; break; } ASSERT_TRUE(aTargetEdge.IsValid()); aGraph.Editor().CoEdges().SetEdgeDefId(aSeamCoEdge, aTargetEdge); // The OTHER coedge of aSeamWire still references aSeamEdge -> wire still bound. bool stillBound = false; for (const BRepGraph_WireId& aW : aGraph.Topo().Edges().Wires(aSeamEdge)) if (aW == aSeamWire) stillBound = true; EXPECT_TRUE(stillBound); EXPECT_TRUE(aGraph.ValidateReverseIndex()); } TEST(BRepGraphIncTest, ReverseIndex_OrphanRef_NoRevIndexUpdate) { // SetRefVertexDefId on a face-direct vertex ref must not touch VertexToEdges // (no map exists for face-direct vertices). Validate consistency afterwards. BRepPrimAPI_MakeBox aBoxMaker(10.0, 20.0, 30.0); BRepGraph aGraph; aGraph.Clear(); [[maybe_unused]] const BRepGraph_Builder::Result aBuildRes = BRepGraph_Builder::Add(aGraph, aBoxMaker.Shape()); ASSERT_TRUE(aGraph.IsDone()); const BRepGraph_VertexId aV = aGraph.Editor().Vertices().Add(gp_Pnt(7, 7, 7), 1.e-7); const BRepGraph_VertexRefId aFaceVtxRef = aGraph.Editor().Faces().AddVertex(BRepGraph_FaceId::Start(), aV, TopAbs_INTERNAL); ASSERT_TRUE(aFaceVtxRef.IsValid()); const BRepGraph_VertexId aV2 = aGraph.Editor().Vertices().Add(gp_Pnt(8, 8, 8), 1.e-7); aGraph.Editor().Vertices().SetRefVertexDefId(aFaceVtxRef, aV2); EXPECT_TRUE(aGraph.ValidateReverseIndex()); EXPECT_EQ(aGraph.Refs().Vertices().Entry(aFaceVtxRef).VertexDefId, aV2); } TEST(BRepGraphIncTest, ReverseIndex_RemoveRef_UnbindsByKind) { // GenOps::RemoveRef must unbind the corresponding rev-index entry. Picks one // FaceRef on the box's first shell; after RemoveRef the FaceToShells entry // for the detached face no longer lists this shell. BRepPrimAPI_MakeBox aBoxMaker(10.0, 20.0, 30.0); BRepGraph aGraph; aGraph.Clear(); [[maybe_unused]] const BRepGraph_Builder::Result aBuildRes = BRepGraph_Builder::Add(aGraph, aBoxMaker.Shape()); ASSERT_TRUE(aGraph.IsDone()); const NCollection_DynamicArray aFaceRefs = BRepGraph_TestTools::FaceRefsOfShell(aGraph, BRepGraph_ShellId::Start()); ASSERT_GE(aFaceRefs.Length(), 1); const BRepGraph_FaceRefId aFaceRefId = aFaceRefs.Value(0); const BRepGraphInc::FaceRef& aRef = aGraph.Refs().Faces().Entry(aFaceRefId); const BRepGraph_FaceId aFaceId = aRef.FaceDefId; ASSERT_TRUE(aGraph.Editor().Gen().RemoveRef(aFaceRefId)); for (const BRepGraph_ShellId& aShellId : aGraph.Topo().Faces().Shells(aFaceId)) EXPECT_NE(aShellId, BRepGraph_ShellId::Start()); EXPECT_TRUE(aGraph.ValidateReverseIndex()); }