Shape Healing - Refactor shape replacement logic to handle cycles (#1247)

Apply some fixes where aaa82fc4de introduce as a regressions
This commit is contained in:
Pasukhin Dmitry
2026-04-30 00:01:00 +01:00
committed by GitHub
parent 91be8c4c71
commit 883cece8e3
7 changed files with 65 additions and 85 deletions
@@ -200,48 +200,10 @@ void BRepTools_ReShape::replace(const TopoDS_Shape& ashape,
std::cout << "Warning: BRepTools_ReShape::Replace: shape already recorded" << std::endl;
#endif
// Reject replacements that would introduce a cycle into the replacement chain,
// e.g. A -> ... -> X -> A. Walk forward from newshape via Value(); if the walk
// ever lands on shape itself, record only an identity (effectively no-op) to
// avoid forming a cycle that would later deadlock Apply()/ValueLeaf().
if (theKind != TReplacementKind_Remove && !newshape.IsNull() && !newshape.IsPartner(shape))
{
// Reject replacements that would close a cycle in the map. Walk forward from
// newshape via Value(); if the chain ever lands back on shape's underlying TShape
// (any orientation, any location), abort. Key by the TShape handle to mirror the
// identity the map itself uses once orientation/location are normalized away.
TopoDS_Shape aProbe = newshape;
NCollection_Map<occ::handle<TopoDS_TShape>> aSeen;
aSeen.Add(shape.TShape());
aSeen.Add(aProbe.TShape());
bool aCycle = false;
for (;;)
{
const TopoDS_Shape aNext = Value(aProbe);
if (aNext.IsNull() || aNext.IsSame(aProbe))
{
break;
}
if (aNext.IsPartner(shape))
{
aCycle = true;
break;
}
if (!aSeen.Add(aNext.TShape()))
{
break; // existing cycle in data - not ours to introduce, bail
}
aProbe = aNext;
}
if (aCycle)
{
#ifdef OCCT_DEBUG
std::cout << "Warning: BRepTools_ReShape::Replace: cycle rejected" << std::endl;
#endif
return;
}
}
// Cycle handling: cycles in the replacement map (A -> B -> A or longer) are
// accepted at insertion time. The DFS in-flight guard inside Apply()/applyImpl()
// breaks any cycle at traversal time, returning the immediate Value() instead
// of recursing.
myShapeToReplacement.Bind(shape, TReplacement(newshape, theKind));
myNewShapes.Add(newshape);
}
@@ -78,24 +78,30 @@ TEST(BRepTools_ReShapeTest, ValueLeaf_ChainEndingInRemoveReturnsNull)
EXPECT_TRUE(aReShape.ValueLeaf(aA).IsNull());
}
// A direct A->B then B->A would close a cycle in the replacement map.
// The second Replace() must be rejected; the first stays intact.
TEST(BRepTools_ReShapeTest, Replace_RejectsDirectCycle)
// A direct A->B then B->A creates a cycle in the replacement map; both bindings
// are recorded, and the DFS in-flight guard inside Apply() handles termination.
TEST(BRepTools_ReShapeTest, Replace_DirectCycleHandledByApply)
{
const TopoDS_Vertex aA = MakeVertex(0, 0, 0);
const TopoDS_Vertex aB = MakeVertex(1, 0, 0);
BRepTools_ReShape aReShape;
aReShape.Replace(aA, aB);
aReShape.Replace(aB, aA); // would create A -> B -> A
aReShape.Replace(aB, aA);
EXPECT_TRUE(aReShape.Value(aA).IsSame(aB)) << "First replacement must remain in effect";
EXPECT_TRUE(aReShape.Value(aB).IsSame(aB)) << "Cyclic second replacement must have been rejected";
EXPECT_TRUE(aReShape.ValueLeaf(aA).IsSame(aB)) << "Chain must terminate, not loop";
EXPECT_TRUE(aReShape.Value(aA).IsSame(aB));
EXPECT_TRUE(aReShape.Value(aB).IsSame(aA));
// Apply() must terminate (DFS in-flight guard) and return a defined shape.
const TopoDS_Shape aResult = aReShape.Apply(aA);
EXPECT_FALSE(aResult.IsNull()) << "Apply must terminate via the DFS guard";
}
// A longer cycle A -> B -> C -> A must also be rejected at the closing edge.
TEST(BRepTools_ReShapeTest, Replace_RejectsLongerCycle)
// Longer cycles A -> B -> C -> A are accepted at Replace() time and broken at
// traversal time by the DFS in-flight guard inside Apply(). This avoids
// over-rejection in IGES/STEP healing pipelines where shapes legitimately
// alias TShapes across stages.
TEST(BRepTools_ReShapeTest, Replace_LongerCycleHandledByApply)
{
const TopoDS_Vertex aA = MakeVertex(0, 0, 0);
const TopoDS_Vertex aB = MakeVertex(1, 0, 0);
@@ -104,10 +110,15 @@ TEST(BRepTools_ReShapeTest, Replace_RejectsLongerCycle)
BRepTools_ReShape aReShape;
aReShape.Replace(aA, aB);
aReShape.Replace(aB, aC);
aReShape.Replace(aC, aA); // would create A -> B -> C -> A
aReShape.Replace(aC, aA);
EXPECT_TRUE(aReShape.ValueLeaf(aA).IsSame(aC)) << "Chain terminates at C; no cycle formed";
EXPECT_TRUE(aReShape.Value(aC).IsSame(aC)) << "Closing replacement was rejected";
EXPECT_TRUE(aReShape.Value(aA).IsSame(aB));
EXPECT_TRUE(aReShape.Value(aB).IsSame(aC));
EXPECT_TRUE(aReShape.Value(aC).IsSame(aA));
// Apply() must terminate (DFS in-flight guard) and return a defined shape.
const TopoDS_Shape aResult = aReShape.Apply(aA);
EXPECT_FALSE(aResult.IsNull()) << "Apply must terminate via the DFS guard";
}
// Apply() must not stack-overflow when a shape's replacement is a compound that