diff --git a/src/Visualization/TKV3d/AIS/AIS_ColoredShape.cxx b/src/Visualization/TKV3d/AIS/AIS_ColoredShape.cxx index bd6cf392b5..8b7810c141 100644 --- a/src/Visualization/TKV3d/AIS/AIS_ColoredShape.cxx +++ b/src/Visualization/TKV3d/AIS/AIS_ColoredShape.cxx @@ -17,6 +17,7 @@ #include #include +#include #include #include #include @@ -745,10 +746,7 @@ bool AIS_ColoredShape::dispatchColors( } // iterate on sub-shapes - BRep_Builder aBBuilder; - TopoDS_Shape aShapeCopy = theShapeToParse.EmptyCopied(); - aShapeCopy.Closed(theShapeToParse.Closed()); - int nbDef = 0; + NCollection_DynamicArray aSubShapes(theShapeToParse.NbChildren()); for (TopoDS_Iterator aSubShapeIter(theShapeToParse); aSubShapeIter.More(); aSubShapeIter.Next()) { const TopoDS_Shape& aSubShape = aSubShapeIter.Value(); @@ -762,20 +760,31 @@ bool AIS_ColoredShape::dispatchColors( { isSubOverride = true; } - else + else if (aShapeType != TopAbs_FACE) { - aBBuilder.Add(aShapeCopy, aSubShape); - ++nbDef; + aSubShapes.Append(aSubShape); } } + + TopoDS_Shape aShapeCopy; + BRep_Builder aBBuilder; if (aShapeType == TopAbs_FACE || !isSubOverride) { aShapeCopy = theShapeToParse; } - else if (nbDef == 0) + else if (aSubShapes.IsEmpty()) { return isOverriden || isSubOverride; // empty compound } + else + { + aShapeCopy = theShapeToParse.EmptyCopied(); + aShapeCopy.Closed(theShapeToParse.Closed()); + for (const TopoDS_Shape& aSubShape : aSubShapes) + { + aBBuilder.Add(aShapeCopy, aSubShape); + } + } // if any of styles is overridden regarding to default one, add rest to map if (isOverriden diff --git a/src/Visualization/TKV3d/GTests/AIS_ColoredShape_Test.cxx b/src/Visualization/TKV3d/GTests/AIS_ColoredShape_Test.cxx new file mode 100644 index 0000000000..7642a8039c --- /dev/null +++ b/src/Visualization/TKV3d/GTests/AIS_ColoredShape_Test.cxx @@ -0,0 +1,326 @@ +// Copyright (c) 2025 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 + +//! Test accessor exposing protected members of AIS_ColoredShape. +class AIS_ColoredShapeTestAccessor : public AIS_ColoredShape +{ +public: + using DrawerCompdMap = AIS_ColoredShape::DataMapOfDrawerCompd; + + AIS_ColoredShapeTestAccessor(const TopoDS_Shape& theShape) + : AIS_ColoredShape(theShape) + { + } + + static bool testDispatchColors( + const occ::handle& theParentDrawer, + const TopoDS_Shape& theShapeToParse, + const NCollection_DataMap, + TopTools_ShapeMapHasher>& theShapeDrawerMap, + const TopAbs_ShapeEnum theParentType, + const bool theIsParentClosed, + DataMapOfDrawerCompd* theDrawerOpenedShapePerType, + DataMapOfDrawerCompd& theDrawerClosedFaces) + { + return dispatchColors(theParentDrawer, + theShapeToParse, + theShapeDrawerMap, + theParentType, + theIsParentClosed, + theDrawerOpenedShapePerType, + theDrawerClosedFaces); + } +}; + +namespace +{ + +//! Count the total number of sub-shapes across all compounds in the dispatch map. +static int countDispatchedShapes(const AIS_ColoredShapeTestAccessor::DrawerCompdMap& theMap) +{ + int aCount = 0; + for (AIS_ColoredShapeTestAccessor::DrawerCompdMap::Iterator anIter(theMap); anIter.More(); + anIter.Next()) + { + for (TopoDS_Iterator aChildIter(anIter.Value()); aChildIter.More(); aChildIter.Next()) + { + ++aCount; + } + } + return aCount; +} + +//! Count the number of faces in a shape. +static int countFaces(const TopoDS_Shape& theShape) +{ + int aCount = 0; + for (TopExp_Explorer anExp(theShape, TopAbs_FACE); anExp.More(); anExp.Next()) + { + ++aCount; + } + return aCount; +} + +} // namespace + +class AIS_ColoredShapeDispatchTest : public testing::Test +{ +protected: + void SetUp() override + { + // Create a box with 6 faces + myBox = BRepPrimAPI_MakeBox(10.0, 20.0, 30.0).Shape(); + ASSERT_FALSE(myBox.IsNull()); + ASSERT_EQ(countFaces(myBox), 6); + + // Collect faces for individual manipulation + for (TopExp_Explorer anExp(myBox, TopAbs_FACE); anExp.More(); anExp.Next()) + { + myFaces.Append(anExp.Current()); + } + + myDefaultDrawer = new Prs3d_Drawer(); + } + + TopoDS_Shape myBox; + NCollection_Sequence myFaces; + occ::handle myDefaultDrawer; +}; + +// Test: no overrides on a shell -> all faces dispatched, no override reported. +TEST_F(AIS_ColoredShapeDispatchTest, Shell_NoOverrides) +{ + NCollection_DataMap, TopTools_ShapeMapHasher> + aShapeDrawerMap; + AIS_ColoredShapeTestAccessor::DrawerCompdMap aOpenedPerType[(size_t)TopAbs_SHAPE]; + AIS_ColoredShapeTestAccessor::DrawerCompdMap aClosedFaces; + + occ::handle aParentDrawer; + // Dispatch on the box shell (first shell child of solid) + for (TopoDS_Iterator anIter(myBox); anIter.More(); anIter.Next()) + { + if (anIter.Value().ShapeType() == TopAbs_SHELL) + { + const bool isOverride = AIS_ColoredShapeTestAccessor::testDispatchColors(aParentDrawer, + anIter.Value(), + aShapeDrawerMap, + TopAbs_SOLID, + false, + aOpenedPerType, + aClosedFaces); + EXPECT_FALSE(isOverride); + break; + } + } + + // All 6 faces should be dispatched as opened (not closed, since theIsParentClosed = false) + EXPECT_EQ(countDispatchedShapes(aOpenedPerType[(size_t)TopAbs_FACE]), 6); + EXPECT_EQ(countDispatchedShapes(aClosedFaces), 0); +} + +// Test: one face overridden with custom color. +TEST_F(AIS_ColoredShapeDispatchTest, Shell_OneFaceOverridden) +{ + NCollection_DataMap, TopTools_ShapeMapHasher> + aShapeDrawerMap; + + // Override a single face + occ::handle aCustomDrawer = new AIS_ColoredDrawer(myDefaultDrawer); + aCustomDrawer->SetOwnColor(Quantity_NOC_RED); + aShapeDrawerMap.Bind(myFaces.First(), aCustomDrawer); + + AIS_ColoredShapeTestAccessor::DrawerCompdMap aOpenedPerType[(size_t)TopAbs_SHAPE]; + AIS_ColoredShapeTestAccessor::DrawerCompdMap aClosedFaces; + + occ::handle aParentDrawer; + for (TopoDS_Iterator anIter(myBox); anIter.More(); anIter.Next()) + { + if (anIter.Value().ShapeType() == TopAbs_SHELL) + { + const bool isOverride = AIS_ColoredShapeTestAccessor::testDispatchColors(aParentDrawer, + anIter.Value(), + aShapeDrawerMap, + TopAbs_SOLID, + false, + aOpenedPerType, + aClosedFaces); + EXPECT_TRUE(isOverride); + break; + } + } + + // All 6 faces dispatched: 1 with custom drawer + 5 with default + EXPECT_EQ(countDispatchedShapes(aOpenedPerType[(size_t)TopAbs_FACE]), 6); +} + +// Test: one face hidden -> override reported, only 5 faces dispatched. +TEST_F(AIS_ColoredShapeDispatchTest, Shell_OneFaceHidden) +{ + NCollection_DataMap, TopTools_ShapeMapHasher> + aShapeDrawerMap; + + // Hide one face + occ::handle aHiddenDrawer = new AIS_ColoredDrawer(myDefaultDrawer); + aHiddenDrawer->SetHidden(true); + aShapeDrawerMap.Bind(myFaces.First(), aHiddenDrawer); + + AIS_ColoredShapeTestAccessor::DrawerCompdMap aOpenedPerType[(size_t)TopAbs_SHAPE]; + AIS_ColoredShapeTestAccessor::DrawerCompdMap aClosedFaces; + + occ::handle aParentDrawer; + for (TopoDS_Iterator anIter(myBox); anIter.More(); anIter.Next()) + { + if (anIter.Value().ShapeType() == TopAbs_SHELL) + { + const bool isOverride = AIS_ColoredShapeTestAccessor::testDispatchColors(aParentDrawer, + anIter.Value(), + aShapeDrawerMap, + TopAbs_SOLID, + false, + aOpenedPerType, + aClosedFaces); + EXPECT_TRUE(isOverride); + break; + } + } + + // Only 5 visible faces dispatched + EXPECT_EQ(countDispatchedShapes(aOpenedPerType[(size_t)TopAbs_FACE]), 5); +} + +// Test: all faces overridden -> all dispatched individually. +TEST_F(AIS_ColoredShapeDispatchTest, Shell_AllFacesOverridden) +{ + NCollection_DataMap, TopTools_ShapeMapHasher> + aShapeDrawerMap; + + // Override all faces + for (NCollection_Sequence::Iterator aFaceIter(myFaces); aFaceIter.More(); + aFaceIter.Next()) + { + occ::handle aDrawer = new AIS_ColoredDrawer(myDefaultDrawer); + aDrawer->SetOwnColor(Quantity_NOC_GREEN); + aShapeDrawerMap.Bind(aFaceIter.Value(), aDrawer); + } + + AIS_ColoredShapeTestAccessor::DrawerCompdMap aOpenedPerType[(size_t)TopAbs_SHAPE]; + AIS_ColoredShapeTestAccessor::DrawerCompdMap aClosedFaces; + + occ::handle aParentDrawer; + for (TopoDS_Iterator anIter(myBox); anIter.More(); anIter.Next()) + { + if (anIter.Value().ShapeType() == TopAbs_SHELL) + { + const bool isOverride = AIS_ColoredShapeTestAccessor::testDispatchColors(aParentDrawer, + anIter.Value(), + aShapeDrawerMap, + TopAbs_SOLID, + false, + aOpenedPerType, + aClosedFaces); + EXPECT_TRUE(isOverride); + break; + } + } + + // All 6 faces dispatched with their custom drawers + EXPECT_EQ(countDispatchedShapes(aOpenedPerType[(size_t)TopAbs_FACE]), 6); +} + +// Test: compound of two boxes, one box's shell overridden. +TEST_F(AIS_ColoredShapeDispatchTest, Compound_PartialOverride) +{ + TopoDS_Shape aBox2 = BRepPrimAPI_MakeBox(5.0, 5.0, 5.0).Shape(); + + BRep_Builder aBBuilder; + TopoDS_Compound aCompound; + aBBuilder.MakeCompound(aCompound); + aBBuilder.Add(aCompound, myBox); + aBBuilder.Add(aCompound, aBox2); + + NCollection_DataMap, TopTools_ShapeMapHasher> + aShapeDrawerMap; + + // Override one face from the first box + occ::handle aCustomDrawer = new AIS_ColoredDrawer(myDefaultDrawer); + aCustomDrawer->SetOwnColor(Quantity_NOC_BLUE); + aShapeDrawerMap.Bind(myFaces.First(), aCustomDrawer); + + AIS_ColoredShapeTestAccessor::DrawerCompdMap aOpenedPerType[(size_t)TopAbs_SHAPE]; + AIS_ColoredShapeTestAccessor::DrawerCompdMap aClosedFaces; + + occ::handle aParentDrawer; + const bool isOverride = AIS_ColoredShapeTestAccessor::testDispatchColors(aParentDrawer, + aCompound, + aShapeDrawerMap, + TopAbs_COMPOUND, + false, + aOpenedPerType, + aClosedFaces); + EXPECT_TRUE(isOverride); + + // 12 faces total (6+6), all should be dispatched + EXPECT_EQ(countDispatchedShapes(aOpenedPerType[(size_t)TopAbs_FACE]), 12); +} + +// Test: all faces hidden in a shell -> early return, empty dispatch. +TEST_F(AIS_ColoredShapeDispatchTest, Shell_AllFacesHidden) +{ + NCollection_DataMap, TopTools_ShapeMapHasher> + aShapeDrawerMap; + + // Hide all faces + for (NCollection_Sequence::Iterator aFaceIter(myFaces); aFaceIter.More(); + aFaceIter.Next()) + { + occ::handle aDrawer = new AIS_ColoredDrawer(myDefaultDrawer); + aDrawer->SetHidden(true); + aShapeDrawerMap.Bind(aFaceIter.Value(), aDrawer); + } + + AIS_ColoredShapeTestAccessor::DrawerCompdMap aOpenedPerType[(size_t)TopAbs_SHAPE]; + AIS_ColoredShapeTestAccessor::DrawerCompdMap aClosedFaces; + + occ::handle aParentDrawer; + for (TopoDS_Iterator anIter(myBox); anIter.More(); anIter.Next()) + { + if (anIter.Value().ShapeType() == TopAbs_SHELL) + { + const bool isOverride = AIS_ColoredShapeTestAccessor::testDispatchColors(aParentDrawer, + anIter.Value(), + aShapeDrawerMap, + TopAbs_SOLID, + false, + aOpenedPerType, + aClosedFaces); + EXPECT_TRUE(isOverride); + break; + } + } + + // No faces dispatched - all hidden + EXPECT_EQ(countDispatchedShapes(aOpenedPerType[(size_t)TopAbs_FACE]), 0); + EXPECT_EQ(countDispatchedShapes(aClosedFaces), 0); +} diff --git a/src/Visualization/TKV3d/GTests/FILES.cmake b/src/Visualization/TKV3d/GTests/FILES.cmake index afdb096c5f..0ee4765087 100644 --- a/src/Visualization/TKV3d/GTests/FILES.cmake +++ b/src/Visualization/TKV3d/GTests/FILES.cmake @@ -2,4 +2,5 @@ set(OCCT_TKV3d_GTests_FILES_LOCATION "${CMAKE_CURRENT_LIST_DIR}") set(OCCT_TKV3d_GTests_FILES + AIS_ColoredShape_Test.cxx )