Modeling Data - Fix curve concatenation to use actual endpoints (#926)

GeomConvert_CompCurveToBSplineCurve::Add() was incorrectly using
  first/last poles for G0 continuity checks instead of actual curve
  endpoints. For non-clamped or periodic B-splines, poles may not
  coincide with curve start/end points, causing concatenation to fail
  or produce incorrect results.

Changed to use StartPoint()/EndPoint() methods which properly
  evaluate the curve at its parameter bounds.
This commit is contained in:
Pasukhin Dmitry
2025-12-20 20:57:27 +00:00
committed by GitHub
parent 915340f72c
commit 568ced2558
3 changed files with 391 additions and 7 deletions

View File

@@ -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
)

View File

@@ -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 <gtest/gtest.h>
#include <Geom_BSplineCurve.hxx>
#include <Geom_Circle.hxx>
#include <Geom_Line.hxx>
#include <Geom_TrimmedCurve.hxx>
#include <GeomConvert.hxx>
#include <GeomConvert_CompCurveToBSplineCurve.hxx>
#include <gp_Ax2.hxx>
#include <gp_Circ.hxx>
#include <gp_Dir.hxx>
#include <gp_Pnt.hxx>
#include <Precision.hxx>
#include <TColgp_Array1OfPnt.hxx>
#include <TColStd_Array1OfInteger.hxx>
#include <TColStd_Array1OfReal.hxx>
//==================================================================================================
// 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";
}

View File

@@ -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();
}