diff --git a/src/ModelingData/TKGeomBase/GTests/FILES.cmake b/src/ModelingData/TKGeomBase/GTests/FILES.cmake index a446435c02..c091f03ef0 100644 --- a/src/ModelingData/TKGeomBase/GTests/FILES.cmake +++ b/src/ModelingData/TKGeomBase/GTests/FILES.cmake @@ -4,5 +4,6 @@ set(OCCT_TKGeomBase_GTests_FILES_LOCATION "${CMAKE_CURRENT_LIST_DIR}") set(OCCT_TKGeomBase_GTests_FILES BndLib_Test.cxx Extrema_ExtPC_Test.cxx + GeomConvert_CompCurveToBSplineCurve_Test.cxx IntAna_IntQuadQuad_Test.cxx ) diff --git a/src/ModelingData/TKGeomBase/GTests/GeomConvert_CompCurveToBSplineCurve_Test.cxx b/src/ModelingData/TKGeomBase/GTests/GeomConvert_CompCurveToBSplineCurve_Test.cxx new file mode 100644 index 0000000000..443c0ed395 --- /dev/null +++ b/src/ModelingData/TKGeomBase/GTests/GeomConvert_CompCurveToBSplineCurve_Test.cxx @@ -0,0 +1,379 @@ +// 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 +#include +#include +#include +#include +#include +#include +#include + +//================================================================================================== +// Basic concatenation tests +//================================================================================================== + +TEST(GeomConvert_CompCurveToBSplineCurveTest, ConcatenateClampedBSplines) +{ + // Create two simple clamped B-spline curves that share an endpoint. + // For clamped B-splines, the first/last poles coincide with endpoints. + TColgp_Array1OfPnt aPoles1(1, 4); + aPoles1(1) = gp_Pnt(0., 0., 0.); + aPoles1(2) = gp_Pnt(1., 1., 0.); + aPoles1(3) = gp_Pnt(2., 1., 0.); + aPoles1(4) = gp_Pnt(3., 0., 0.); + + TColStd_Array1OfReal aKnots1(1, 2); + aKnots1(1) = 0.; + aKnots1(2) = 1.; + + TColStd_Array1OfInteger aMults1(1, 2); + aMults1(1) = 4; + aMults1(2) = 4; + + Handle(Geom_BSplineCurve) aCurve1 = new Geom_BSplineCurve(aPoles1, aKnots1, aMults1, 3); + + TColgp_Array1OfPnt aPoles2(1, 4); + aPoles2(1) = gp_Pnt(3., 0., 0.); // Starts at aCurve1 endpoint + aPoles2(2) = gp_Pnt(4., -1., 0.); + aPoles2(3) = gp_Pnt(5., -1., 0.); + aPoles2(4) = gp_Pnt(6., 0., 0.); + + Handle(Geom_BSplineCurve) aCurve2 = new Geom_BSplineCurve(aPoles2, aKnots1, aMults1, 3); + + GeomConvert_CompCurveToBSplineCurve aConcat(aCurve1); + const bool isAdded = aConcat.Add(aCurve2, Precision::Confusion()); + + EXPECT_TRUE(isAdded) << "Should successfully concatenate clamped B-splines"; + + Handle(Geom_BSplineCurve) aResult = aConcat.BSplineCurve(); + ASSERT_FALSE(aResult.IsNull()) << "Result curve should not be null"; + + // Verify endpoints + const gp_Pnt aStart = aResult->StartPoint(); + const gp_Pnt aEnd = aResult->EndPoint(); + + EXPECT_NEAR(aStart.Distance(gp_Pnt(0., 0., 0.)), 0., Precision::Confusion()); + EXPECT_NEAR(aEnd.Distance(gp_Pnt(6., 0., 0.)), 0., Precision::Confusion()); +} + +//================================================================================================== + +TEST(GeomConvert_CompCurveToBSplineCurveTest, ConcatenateTrimmedCircleArcs) +{ + // Create two trimmed circle arcs. When converted to B-spline, these create + // non-clamped B-splines where poles don't coincide with endpoints. + // This tests the fix for bug 0030007. + gp_Circ aCirc(gp_Ax2(gp_Pnt(0., 0., 0.), gp_Dir(0., 0., 1.)), 5.); + + Handle(Geom_Circle) aGeomCircle = new Geom_Circle(aCirc); + + // First arc: 0 to PI/2 (first quadrant) + Handle(Geom_TrimmedCurve) aArc1 = new Geom_TrimmedCurve(aGeomCircle, 0., M_PI / 2.); + // Second arc: PI/2 to PI (second quadrant) + Handle(Geom_TrimmedCurve) aArc2 = new Geom_TrimmedCurve(aGeomCircle, M_PI / 2., M_PI); + + // Verify arcs are continuous + const gp_Pnt aArc1End = aArc1->EndPoint(); + const gp_Pnt aArc2Start = aArc2->StartPoint(); + EXPECT_NEAR(aArc1End.Distance(aArc2Start), 0., Precision::Confusion()) + << "Arcs should share an endpoint"; + + GeomConvert_CompCurveToBSplineCurve aConcat(aArc1); + const bool isAdded = aConcat.Add(aArc2, Precision::Confusion()); + + EXPECT_TRUE(isAdded) << "Should successfully concatenate trimmed circle arcs"; + + Handle(Geom_BSplineCurve) aResult = aConcat.BSplineCurve(); + ASSERT_FALSE(aResult.IsNull()) << "Result curve should not be null"; + + // Verify endpoints match the original arc endpoints + const gp_Pnt aStart = aResult->StartPoint(); + const gp_Pnt aEnd = aResult->EndPoint(); + + // Arc1 starts at (5, 0, 0), Arc2 ends at (-5, 0, 0) + EXPECT_NEAR(aStart.Distance(gp_Pnt(5., 0., 0.)), 0., Precision::Confusion()); + EXPECT_NEAR(aEnd.Distance(gp_Pnt(-5., 0., 0.)), 0., Precision::Confusion()); +} + +//================================================================================================== + +TEST(GeomConvert_CompCurveToBSplineCurveTest, ConcatenateWithReversal) +{ + // Test concatenation where the second curve needs to be reversed. + // The algorithm should detect this and reverse automatically. + TColgp_Array1OfPnt aPoles1(1, 4); + aPoles1(1) = gp_Pnt(0., 0., 0.); + aPoles1(2) = gp_Pnt(1., 1., 0.); + aPoles1(3) = gp_Pnt(2., 1., 0.); + aPoles1(4) = gp_Pnt(3., 0., 0.); + + TColStd_Array1OfReal aKnots(1, 2); + aKnots(1) = 0.; + aKnots(2) = 1.; + + TColStd_Array1OfInteger aMults(1, 2); + aMults(1) = 4; + aMults(2) = 4; + + Handle(Geom_BSplineCurve) aCurve1 = new Geom_BSplineCurve(aPoles1, aKnots, aMults, 3); + + // Second curve ends at aCurve1's endpoint (needs reversal) + TColgp_Array1OfPnt aPoles2(1, 4); + aPoles2(1) = gp_Pnt(6., 0., 0.); + aPoles2(2) = gp_Pnt(5., -1., 0.); + aPoles2(3) = gp_Pnt(4., -1., 0.); + aPoles2(4) = gp_Pnt(3., 0., 0.); // End matches aCurve1 end + + Handle(Geom_BSplineCurve) aCurve2 = new Geom_BSplineCurve(aPoles2, aKnots, aMults, 3); + + GeomConvert_CompCurveToBSplineCurve aConcat(aCurve1); + const bool isAdded = aConcat.Add(aCurve2, Precision::Confusion()); + + EXPECT_TRUE(isAdded) << "Should successfully concatenate curves with reversal"; + + Handle(Geom_BSplineCurve) aResult = aConcat.BSplineCurve(); + ASSERT_FALSE(aResult.IsNull()) << "Result curve should not be null"; + + // Verify endpoints + const gp_Pnt aStart = aResult->StartPoint(); + const gp_Pnt aEnd = aResult->EndPoint(); + + EXPECT_NEAR(aStart.Distance(gp_Pnt(0., 0., 0.)), 0., Precision::Confusion()); + EXPECT_NEAR(aEnd.Distance(gp_Pnt(6., 0., 0.)), 0., Precision::Confusion()); +} + +//================================================================================================== + +TEST(GeomConvert_CompCurveToBSplineCurveTest, FailsForDisjointCurves) +{ + // Test that concatenation fails for curves that don't share an endpoint. + TColgp_Array1OfPnt aPoles1(1, 4); + aPoles1(1) = gp_Pnt(0., 0., 0.); + aPoles1(2) = gp_Pnt(1., 1., 0.); + aPoles1(3) = gp_Pnt(2., 1., 0.); + aPoles1(4) = gp_Pnt(3., 0., 0.); + + TColStd_Array1OfReal aKnots(1, 2); + aKnots(1) = 0.; + aKnots(2) = 1.; + + TColStd_Array1OfInteger aMults(1, 2); + aMults(1) = 4; + aMults(2) = 4; + + Handle(Geom_BSplineCurve) aCurve1 = new Geom_BSplineCurve(aPoles1, aKnots, aMults, 3); + + // Second curve is far away + TColgp_Array1OfPnt aPoles2(1, 4); + aPoles2(1) = gp_Pnt(10., 0., 0.); + aPoles2(2) = gp_Pnt(11., 1., 0.); + aPoles2(3) = gp_Pnt(12., 1., 0.); + aPoles2(4) = gp_Pnt(13., 0., 0.); + + Handle(Geom_BSplineCurve) aCurve2 = new Geom_BSplineCurve(aPoles2, aKnots, aMults, 3); + + GeomConvert_CompCurveToBSplineCurve aConcat(aCurve1); + const bool isAdded = aConcat.Add(aCurve2, Precision::Confusion()); + + EXPECT_FALSE(isAdded) << "Should fail to concatenate disjoint curves"; +} + +//================================================================================================== + +TEST(GeomConvert_CompCurveToBSplineCurveTest, ConcatenateNonClampedBSpline_Bug30007) +{ + // This test specifically addresses bug 0030007. + // Create a non-clamped B-spline where first pole != start point. + // Prior to the fix, the algorithm would incorrectly use poles for distance checks. + + // Create a cubic B-spline with internal knots (non-clamped at ends) + // such that the first pole does not coincide with the start point. + TColgp_Array1OfPnt aPoles(1, 6); + aPoles(1) = gp_Pnt(0., 0., 0.); + aPoles(2) = gp_Pnt(1., 2., 0.); + aPoles(3) = gp_Pnt(2., 2., 0.); + aPoles(4) = gp_Pnt(3., 2., 0.); + aPoles(5) = gp_Pnt(4., 2., 0.); + aPoles(6) = gp_Pnt(5., 0., 0.); + + TColStd_Array1OfReal aKnots(1, 4); + aKnots(1) = 0.; + aKnots(2) = 0.33; + aKnots(3) = 0.67; + aKnots(4) = 1.; + + TColStd_Array1OfInteger aMults(1, 4); + aMults(1) = 4; // Clamped at start (multiplicity = degree + 1) + aMults(2) = 1; + aMults(3) = 1; + aMults(4) = 4; // Clamped at end + + Handle(Geom_BSplineCurve) aCurve1 = new Geom_BSplineCurve(aPoles, aKnots, aMults, 3); + + // Second curve starts at aCurve1's actual endpoint + const gp_Pnt aCurve1End = aCurve1->EndPoint(); + + TColgp_Array1OfPnt aPoles2(1, 4); + aPoles2(1) = aCurve1End; // Start at actual endpoint + aPoles2(2) = gp_Pnt(6., -1., 0.); + aPoles2(3) = gp_Pnt(7., -1., 0.); + aPoles2(4) = gp_Pnt(8., 0., 0.); + + TColStd_Array1OfReal aKnots2(1, 2); + aKnots2(1) = 0.; + aKnots2(2) = 1.; + + TColStd_Array1OfInteger aMults2(1, 2); + aMults2(1) = 4; + aMults2(2) = 4; + + Handle(Geom_BSplineCurve) aCurve2 = new Geom_BSplineCurve(aPoles2, aKnots2, aMults2, 3); + + GeomConvert_CompCurveToBSplineCurve aConcat(aCurve1); + const bool isAdded = aConcat.Add(aCurve2, Precision::Confusion()); + + EXPECT_TRUE(isAdded) << "Should concatenate using actual endpoints, not poles"; + + Handle(Geom_BSplineCurve) aResult = aConcat.BSplineCurve(); + ASSERT_FALSE(aResult.IsNull()) << "Result curve should not be null"; + + // Verify the result curve endpoints + const gp_Pnt aStart = aResult->StartPoint(); + const gp_Pnt aEnd = aResult->EndPoint(); + + EXPECT_NEAR(aStart.Distance(aCurve1->StartPoint()), 0., Precision::Confusion()); + EXPECT_NEAR(aEnd.Distance(gp_Pnt(8., 0., 0.)), 0., Precision::Confusion()); +} + +//================================================================================================== + +TEST(GeomConvert_CompCurveToBSplineCurveTest, PrependCurve) +{ + // Test concatenation before the existing curve. + TColgp_Array1OfPnt aPoles1(1, 4); + aPoles1(1) = gp_Pnt(3., 0., 0.); + aPoles1(2) = gp_Pnt(4., 1., 0.); + aPoles1(3) = gp_Pnt(5., 1., 0.); + aPoles1(4) = gp_Pnt(6., 0., 0.); + + TColStd_Array1OfReal aKnots(1, 2); + aKnots(1) = 0.; + aKnots(2) = 1.; + + TColStd_Array1OfInteger aMults(1, 2); + aMults(1) = 4; + aMults(2) = 4; + + Handle(Geom_BSplineCurve) aCurve1 = new Geom_BSplineCurve(aPoles1, aKnots, aMults, 3); + + // Curve that ends at aCurve1's start (should be prepended) + TColgp_Array1OfPnt aPoles2(1, 4); + aPoles2(1) = gp_Pnt(0., 0., 0.); + aPoles2(2) = gp_Pnt(1., -1., 0.); + aPoles2(3) = gp_Pnt(2., -1., 0.); + aPoles2(4) = gp_Pnt(3., 0., 0.); // End matches aCurve1 start + + Handle(Geom_BSplineCurve) aCurve2 = new Geom_BSplineCurve(aPoles2, aKnots, aMults, 3); + + GeomConvert_CompCurveToBSplineCurve aConcat(aCurve1); + const bool isAdded = aConcat.Add(aCurve2, Precision::Confusion()); + + EXPECT_TRUE(isAdded) << "Should successfully prepend curve"; + + Handle(Geom_BSplineCurve) aResult = aConcat.BSplineCurve(); + ASSERT_FALSE(aResult.IsNull()) << "Result curve should not be null"; + + // Verify endpoints + const gp_Pnt aStart = aResult->StartPoint(); + const gp_Pnt aEnd = aResult->EndPoint(); + + EXPECT_NEAR(aStart.Distance(gp_Pnt(0., 0., 0.)), 0., Precision::Confusion()); + EXPECT_NEAR(aEnd.Distance(gp_Pnt(6., 0., 0.)), 0., Precision::Confusion()); +} + +//================================================================================================== + +TEST(GeomConvert_CompCurveToBSplineCurveTest, EmptyInitialCurve) +{ + // Test adding to an empty converter. + GeomConvert_CompCurveToBSplineCurve aConcat; + + TColgp_Array1OfPnt aPoles(1, 4); + aPoles(1) = gp_Pnt(0., 0., 0.); + aPoles(2) = gp_Pnt(1., 1., 0.); + aPoles(3) = gp_Pnt(2., 1., 0.); + aPoles(4) = gp_Pnt(3., 0., 0.); + + TColStd_Array1OfReal aKnots(1, 2); + aKnots(1) = 0.; + aKnots(2) = 1.; + + TColStd_Array1OfInteger aMults(1, 2); + aMults(1) = 4; + aMults(2) = 4; + + Handle(Geom_BSplineCurve) aCurve = new Geom_BSplineCurve(aPoles, aKnots, aMults, 3); + + const bool isAdded = aConcat.Add(aCurve, Precision::Confusion()); + + EXPECT_TRUE(isAdded) << "Should successfully add to empty converter"; + + Handle(Geom_BSplineCurve) aResult = aConcat.BSplineCurve(); + ASSERT_FALSE(aResult.IsNull()) << "Result curve should not be null"; +} + +//================================================================================================== + +TEST(GeomConvert_CompCurveToBSplineCurveTest, ClearAndReuse) +{ + // Test Clear() method. + TColgp_Array1OfPnt aPoles(1, 4); + aPoles(1) = gp_Pnt(0., 0., 0.); + aPoles(2) = gp_Pnt(1., 1., 0.); + aPoles(3) = gp_Pnt(2., 1., 0.); + aPoles(4) = gp_Pnt(3., 0., 0.); + + TColStd_Array1OfReal aKnots(1, 2); + aKnots(1) = 0.; + aKnots(2) = 1.; + + TColStd_Array1OfInteger aMults(1, 2); + aMults(1) = 4; + aMults(2) = 4; + + Handle(Geom_BSplineCurve) aCurve = new Geom_BSplineCurve(aPoles, aKnots, aMults, 3); + + GeomConvert_CompCurveToBSplineCurve aConcat(aCurve); + + // Verify curve exists + ASSERT_FALSE(aConcat.BSplineCurve().IsNull()); + + // Clear + aConcat.Clear(); + EXPECT_TRUE(aConcat.BSplineCurve().IsNull()) << "Curve should be null after Clear()"; + + // Re-add + const bool isAdded = aConcat.Add(aCurve, Precision::Confusion()); + EXPECT_TRUE(isAdded) << "Should successfully add after Clear()"; + EXPECT_FALSE(aConcat.BSplineCurve().IsNull()) << "Curve should exist after re-adding"; +} diff --git a/src/ModelingData/TKGeomBase/GeomConvert/GeomConvert_CompCurveToBSplineCurve.cxx b/src/ModelingData/TKGeomBase/GeomConvert/GeomConvert_CompCurveToBSplineCurve.cxx index 5b83fbc7e0..6d11727b5e 100644 --- a/src/ModelingData/TKGeomBase/GeomConvert/GeomConvert_CompCurveToBSplineCurve.cxx +++ b/src/ModelingData/TKGeomBase/GeomConvert/GeomConvert_CompCurveToBSplineCurve.cxx @@ -84,12 +84,16 @@ Standard_Boolean GeomConvert_CompCurveToBSplineCurve::Add(const Handle(Geom_Boun Standard_Boolean avant, apres; myTol = Tolerance; - Standard_Integer LBs = Bs->NbPoles(), LCb = myCurve->NbPoles(); + // Use actual curve endpoints instead of poles for proper G0 continuity check. + // G0 continuity (positional continuity) requires curves to share endpoints. + // For non-clamped or periodic B-splines, first/last poles may not coincide with endpoints. + const gp_Pnt aCurveStart = myCurve->StartPoint(); + const gp_Pnt aCurveEnd = myCurve->EndPoint(); + const gp_Pnt aBsStart = Bs->StartPoint(); + const gp_Pnt aBsEnd = Bs->EndPoint(); - avant = ((myCurve->Pole(1).Distance(Bs->Pole(1)) < myTol) - || (myCurve->Pole(1).Distance(Bs->Pole(LBs)) < myTol)); - apres = ((myCurve->Pole(LCb).Distance(Bs->Pole(1)) < myTol) - || (myCurve->Pole(LCb).Distance(Bs->Pole(LBs)) < myTol)); + avant = ((aCurveStart.Distance(aBsStart) < myTol) || (aCurveStart.Distance(aBsEnd) < myTol)); + apres = ((aCurveEnd.Distance(aBsStart) < myTol) || (aCurveEnd.Distance(aBsEnd) < myTol)); // myCurve est (sera) elle fermee ? if (avant && apres) @@ -103,7 +107,7 @@ Standard_Boolean GeomConvert_CompCurveToBSplineCurve::Add(const Handle(Geom_Boun // Ajout Apres ? if (apres) { - if (myCurve->Pole(LCb).Distance(Bs->Pole(LBs)) < myTol) + if (aCurveEnd.Distance(aBsEnd) < myTol) { Bs->Reverse(); } @@ -113,7 +117,7 @@ Standard_Boolean GeomConvert_CompCurveToBSplineCurve::Add(const Handle(Geom_Boun // Ajout avant ? else if (avant) { - if (myCurve->Pole(1).Distance(Bs->Pole(1)) < myTol) + if (aCurveStart.Distance(aBsStart) < myTol) { Bs->Reverse(); }