Skip to content

Commit 568ced2

Browse files
authored
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.
1 parent 915340f commit 568ced2

3 files changed

Lines changed: 391 additions & 7 deletions

File tree

src/ModelingData/TKGeomBase/GTests/FILES.cmake

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ set(OCCT_TKGeomBase_GTests_FILES_LOCATION "${CMAKE_CURRENT_LIST_DIR}")
44
set(OCCT_TKGeomBase_GTests_FILES
55
BndLib_Test.cxx
66
Extrema_ExtPC_Test.cxx
7+
GeomConvert_CompCurveToBSplineCurve_Test.cxx
78
IntAna_IntQuadQuad_Test.cxx
89
)
Lines changed: 379 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,379 @@
1+
// Copyright (c) 2025 OPEN CASCADE SAS
2+
//
3+
// This file is part of Open CASCADE Technology software library.
4+
//
5+
// This library is free software; you can redistribute it and/or modify it under
6+
// the terms of the GNU Lesser General Public License version 2.1 as published
7+
// by the Free Software Foundation, with special exception defined in the file
8+
// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT
9+
// distribution for complete text of the license and disclaimer of any warranty.
10+
//
11+
// Alternatively, this file may be used under the terms of Open CASCADE
12+
// commercial license or contractual agreement.
13+
14+
#include <gtest/gtest.h>
15+
16+
#include <Geom_BSplineCurve.hxx>
17+
#include <Geom_Circle.hxx>
18+
#include <Geom_Line.hxx>
19+
#include <Geom_TrimmedCurve.hxx>
20+
#include <GeomConvert.hxx>
21+
#include <GeomConvert_CompCurveToBSplineCurve.hxx>
22+
#include <gp_Ax2.hxx>
23+
#include <gp_Circ.hxx>
24+
#include <gp_Dir.hxx>
25+
#include <gp_Pnt.hxx>
26+
#include <Precision.hxx>
27+
#include <TColgp_Array1OfPnt.hxx>
28+
#include <TColStd_Array1OfInteger.hxx>
29+
#include <TColStd_Array1OfReal.hxx>
30+
31+
//==================================================================================================
32+
// Basic concatenation tests
33+
//==================================================================================================
34+
35+
TEST(GeomConvert_CompCurveToBSplineCurveTest, ConcatenateClampedBSplines)
36+
{
37+
// Create two simple clamped B-spline curves that share an endpoint.
38+
// For clamped B-splines, the first/last poles coincide with endpoints.
39+
TColgp_Array1OfPnt aPoles1(1, 4);
40+
aPoles1(1) = gp_Pnt(0., 0., 0.);
41+
aPoles1(2) = gp_Pnt(1., 1., 0.);
42+
aPoles1(3) = gp_Pnt(2., 1., 0.);
43+
aPoles1(4) = gp_Pnt(3., 0., 0.);
44+
45+
TColStd_Array1OfReal aKnots1(1, 2);
46+
aKnots1(1) = 0.;
47+
aKnots1(2) = 1.;
48+
49+
TColStd_Array1OfInteger aMults1(1, 2);
50+
aMults1(1) = 4;
51+
aMults1(2) = 4;
52+
53+
Handle(Geom_BSplineCurve) aCurve1 = new Geom_BSplineCurve(aPoles1, aKnots1, aMults1, 3);
54+
55+
TColgp_Array1OfPnt aPoles2(1, 4);
56+
aPoles2(1) = gp_Pnt(3., 0., 0.); // Starts at aCurve1 endpoint
57+
aPoles2(2) = gp_Pnt(4., -1., 0.);
58+
aPoles2(3) = gp_Pnt(5., -1., 0.);
59+
aPoles2(4) = gp_Pnt(6., 0., 0.);
60+
61+
Handle(Geom_BSplineCurve) aCurve2 = new Geom_BSplineCurve(aPoles2, aKnots1, aMults1, 3);
62+
63+
GeomConvert_CompCurveToBSplineCurve aConcat(aCurve1);
64+
const bool isAdded = aConcat.Add(aCurve2, Precision::Confusion());
65+
66+
EXPECT_TRUE(isAdded) << "Should successfully concatenate clamped B-splines";
67+
68+
Handle(Geom_BSplineCurve) aResult = aConcat.BSplineCurve();
69+
ASSERT_FALSE(aResult.IsNull()) << "Result curve should not be null";
70+
71+
// Verify endpoints
72+
const gp_Pnt aStart = aResult->StartPoint();
73+
const gp_Pnt aEnd = aResult->EndPoint();
74+
75+
EXPECT_NEAR(aStart.Distance(gp_Pnt(0., 0., 0.)), 0., Precision::Confusion());
76+
EXPECT_NEAR(aEnd.Distance(gp_Pnt(6., 0., 0.)), 0., Precision::Confusion());
77+
}
78+
79+
//==================================================================================================
80+
81+
TEST(GeomConvert_CompCurveToBSplineCurveTest, ConcatenateTrimmedCircleArcs)
82+
{
83+
// Create two trimmed circle arcs. When converted to B-spline, these create
84+
// non-clamped B-splines where poles don't coincide with endpoints.
85+
// This tests the fix for bug 0030007.
86+
gp_Circ aCirc(gp_Ax2(gp_Pnt(0., 0., 0.), gp_Dir(0., 0., 1.)), 5.);
87+
88+
Handle(Geom_Circle) aGeomCircle = new Geom_Circle(aCirc);
89+
90+
// First arc: 0 to PI/2 (first quadrant)
91+
Handle(Geom_TrimmedCurve) aArc1 = new Geom_TrimmedCurve(aGeomCircle, 0., M_PI / 2.);
92+
// Second arc: PI/2 to PI (second quadrant)
93+
Handle(Geom_TrimmedCurve) aArc2 = new Geom_TrimmedCurve(aGeomCircle, M_PI / 2., M_PI);
94+
95+
// Verify arcs are continuous
96+
const gp_Pnt aArc1End = aArc1->EndPoint();
97+
const gp_Pnt aArc2Start = aArc2->StartPoint();
98+
EXPECT_NEAR(aArc1End.Distance(aArc2Start), 0., Precision::Confusion())
99+
<< "Arcs should share an endpoint";
100+
101+
GeomConvert_CompCurveToBSplineCurve aConcat(aArc1);
102+
const bool isAdded = aConcat.Add(aArc2, Precision::Confusion());
103+
104+
EXPECT_TRUE(isAdded) << "Should successfully concatenate trimmed circle arcs";
105+
106+
Handle(Geom_BSplineCurve) aResult = aConcat.BSplineCurve();
107+
ASSERT_FALSE(aResult.IsNull()) << "Result curve should not be null";
108+
109+
// Verify endpoints match the original arc endpoints
110+
const gp_Pnt aStart = aResult->StartPoint();
111+
const gp_Pnt aEnd = aResult->EndPoint();
112+
113+
// Arc1 starts at (5, 0, 0), Arc2 ends at (-5, 0, 0)
114+
EXPECT_NEAR(aStart.Distance(gp_Pnt(5., 0., 0.)), 0., Precision::Confusion());
115+
EXPECT_NEAR(aEnd.Distance(gp_Pnt(-5., 0., 0.)), 0., Precision::Confusion());
116+
}
117+
118+
//==================================================================================================
119+
120+
TEST(GeomConvert_CompCurveToBSplineCurveTest, ConcatenateWithReversal)
121+
{
122+
// Test concatenation where the second curve needs to be reversed.
123+
// The algorithm should detect this and reverse automatically.
124+
TColgp_Array1OfPnt aPoles1(1, 4);
125+
aPoles1(1) = gp_Pnt(0., 0., 0.);
126+
aPoles1(2) = gp_Pnt(1., 1., 0.);
127+
aPoles1(3) = gp_Pnt(2., 1., 0.);
128+
aPoles1(4) = gp_Pnt(3., 0., 0.);
129+
130+
TColStd_Array1OfReal aKnots(1, 2);
131+
aKnots(1) = 0.;
132+
aKnots(2) = 1.;
133+
134+
TColStd_Array1OfInteger aMults(1, 2);
135+
aMults(1) = 4;
136+
aMults(2) = 4;
137+
138+
Handle(Geom_BSplineCurve) aCurve1 = new Geom_BSplineCurve(aPoles1, aKnots, aMults, 3);
139+
140+
// Second curve ends at aCurve1's endpoint (needs reversal)
141+
TColgp_Array1OfPnt aPoles2(1, 4);
142+
aPoles2(1) = gp_Pnt(6., 0., 0.);
143+
aPoles2(2) = gp_Pnt(5., -1., 0.);
144+
aPoles2(3) = gp_Pnt(4., -1., 0.);
145+
aPoles2(4) = gp_Pnt(3., 0., 0.); // End matches aCurve1 end
146+
147+
Handle(Geom_BSplineCurve) aCurve2 = new Geom_BSplineCurve(aPoles2, aKnots, aMults, 3);
148+
149+
GeomConvert_CompCurveToBSplineCurve aConcat(aCurve1);
150+
const bool isAdded = aConcat.Add(aCurve2, Precision::Confusion());
151+
152+
EXPECT_TRUE(isAdded) << "Should successfully concatenate curves with reversal";
153+
154+
Handle(Geom_BSplineCurve) aResult = aConcat.BSplineCurve();
155+
ASSERT_FALSE(aResult.IsNull()) << "Result curve should not be null";
156+
157+
// Verify endpoints
158+
const gp_Pnt aStart = aResult->StartPoint();
159+
const gp_Pnt aEnd = aResult->EndPoint();
160+
161+
EXPECT_NEAR(aStart.Distance(gp_Pnt(0., 0., 0.)), 0., Precision::Confusion());
162+
EXPECT_NEAR(aEnd.Distance(gp_Pnt(6., 0., 0.)), 0., Precision::Confusion());
163+
}
164+
165+
//==================================================================================================
166+
167+
TEST(GeomConvert_CompCurveToBSplineCurveTest, FailsForDisjointCurves)
168+
{
169+
// Test that concatenation fails for curves that don't share an endpoint.
170+
TColgp_Array1OfPnt aPoles1(1, 4);
171+
aPoles1(1) = gp_Pnt(0., 0., 0.);
172+
aPoles1(2) = gp_Pnt(1., 1., 0.);
173+
aPoles1(3) = gp_Pnt(2., 1., 0.);
174+
aPoles1(4) = gp_Pnt(3., 0., 0.);
175+
176+
TColStd_Array1OfReal aKnots(1, 2);
177+
aKnots(1) = 0.;
178+
aKnots(2) = 1.;
179+
180+
TColStd_Array1OfInteger aMults(1, 2);
181+
aMults(1) = 4;
182+
aMults(2) = 4;
183+
184+
Handle(Geom_BSplineCurve) aCurve1 = new Geom_BSplineCurve(aPoles1, aKnots, aMults, 3);
185+
186+
// Second curve is far away
187+
TColgp_Array1OfPnt aPoles2(1, 4);
188+
aPoles2(1) = gp_Pnt(10., 0., 0.);
189+
aPoles2(2) = gp_Pnt(11., 1., 0.);
190+
aPoles2(3) = gp_Pnt(12., 1., 0.);
191+
aPoles2(4) = gp_Pnt(13., 0., 0.);
192+
193+
Handle(Geom_BSplineCurve) aCurve2 = new Geom_BSplineCurve(aPoles2, aKnots, aMults, 3);
194+
195+
GeomConvert_CompCurveToBSplineCurve aConcat(aCurve1);
196+
const bool isAdded = aConcat.Add(aCurve2, Precision::Confusion());
197+
198+
EXPECT_FALSE(isAdded) << "Should fail to concatenate disjoint curves";
199+
}
200+
201+
//==================================================================================================
202+
203+
TEST(GeomConvert_CompCurveToBSplineCurveTest, ConcatenateNonClampedBSpline_Bug30007)
204+
{
205+
// This test specifically addresses bug 0030007.
206+
// Create a non-clamped B-spline where first pole != start point.
207+
// Prior to the fix, the algorithm would incorrectly use poles for distance checks.
208+
209+
// Create a cubic B-spline with internal knots (non-clamped at ends)
210+
// such that the first pole does not coincide with the start point.
211+
TColgp_Array1OfPnt aPoles(1, 6);
212+
aPoles(1) = gp_Pnt(0., 0., 0.);
213+
aPoles(2) = gp_Pnt(1., 2., 0.);
214+
aPoles(3) = gp_Pnt(2., 2., 0.);
215+
aPoles(4) = gp_Pnt(3., 2., 0.);
216+
aPoles(5) = gp_Pnt(4., 2., 0.);
217+
aPoles(6) = gp_Pnt(5., 0., 0.);
218+
219+
TColStd_Array1OfReal aKnots(1, 4);
220+
aKnots(1) = 0.;
221+
aKnots(2) = 0.33;
222+
aKnots(3) = 0.67;
223+
aKnots(4) = 1.;
224+
225+
TColStd_Array1OfInteger aMults(1, 4);
226+
aMults(1) = 4; // Clamped at start (multiplicity = degree + 1)
227+
aMults(2) = 1;
228+
aMults(3) = 1;
229+
aMults(4) = 4; // Clamped at end
230+
231+
Handle(Geom_BSplineCurve) aCurve1 = new Geom_BSplineCurve(aPoles, aKnots, aMults, 3);
232+
233+
// Second curve starts at aCurve1's actual endpoint
234+
const gp_Pnt aCurve1End = aCurve1->EndPoint();
235+
236+
TColgp_Array1OfPnt aPoles2(1, 4);
237+
aPoles2(1) = aCurve1End; // Start at actual endpoint
238+
aPoles2(2) = gp_Pnt(6., -1., 0.);
239+
aPoles2(3) = gp_Pnt(7., -1., 0.);
240+
aPoles2(4) = gp_Pnt(8., 0., 0.);
241+
242+
TColStd_Array1OfReal aKnots2(1, 2);
243+
aKnots2(1) = 0.;
244+
aKnots2(2) = 1.;
245+
246+
TColStd_Array1OfInteger aMults2(1, 2);
247+
aMults2(1) = 4;
248+
aMults2(2) = 4;
249+
250+
Handle(Geom_BSplineCurve) aCurve2 = new Geom_BSplineCurve(aPoles2, aKnots2, aMults2, 3);
251+
252+
GeomConvert_CompCurveToBSplineCurve aConcat(aCurve1);
253+
const bool isAdded = aConcat.Add(aCurve2, Precision::Confusion());
254+
255+
EXPECT_TRUE(isAdded) << "Should concatenate using actual endpoints, not poles";
256+
257+
Handle(Geom_BSplineCurve) aResult = aConcat.BSplineCurve();
258+
ASSERT_FALSE(aResult.IsNull()) << "Result curve should not be null";
259+
260+
// Verify the result curve endpoints
261+
const gp_Pnt aStart = aResult->StartPoint();
262+
const gp_Pnt aEnd = aResult->EndPoint();
263+
264+
EXPECT_NEAR(aStart.Distance(aCurve1->StartPoint()), 0., Precision::Confusion());
265+
EXPECT_NEAR(aEnd.Distance(gp_Pnt(8., 0., 0.)), 0., Precision::Confusion());
266+
}
267+
268+
//==================================================================================================
269+
270+
TEST(GeomConvert_CompCurveToBSplineCurveTest, PrependCurve)
271+
{
272+
// Test concatenation before the existing curve.
273+
TColgp_Array1OfPnt aPoles1(1, 4);
274+
aPoles1(1) = gp_Pnt(3., 0., 0.);
275+
aPoles1(2) = gp_Pnt(4., 1., 0.);
276+
aPoles1(3) = gp_Pnt(5., 1., 0.);
277+
aPoles1(4) = gp_Pnt(6., 0., 0.);
278+
279+
TColStd_Array1OfReal aKnots(1, 2);
280+
aKnots(1) = 0.;
281+
aKnots(2) = 1.;
282+
283+
TColStd_Array1OfInteger aMults(1, 2);
284+
aMults(1) = 4;
285+
aMults(2) = 4;
286+
287+
Handle(Geom_BSplineCurve) aCurve1 = new Geom_BSplineCurve(aPoles1, aKnots, aMults, 3);
288+
289+
// Curve that ends at aCurve1's start (should be prepended)
290+
TColgp_Array1OfPnt aPoles2(1, 4);
291+
aPoles2(1) = gp_Pnt(0., 0., 0.);
292+
aPoles2(2) = gp_Pnt(1., -1., 0.);
293+
aPoles2(3) = gp_Pnt(2., -1., 0.);
294+
aPoles2(4) = gp_Pnt(3., 0., 0.); // End matches aCurve1 start
295+
296+
Handle(Geom_BSplineCurve) aCurve2 = new Geom_BSplineCurve(aPoles2, aKnots, aMults, 3);
297+
298+
GeomConvert_CompCurveToBSplineCurve aConcat(aCurve1);
299+
const bool isAdded = aConcat.Add(aCurve2, Precision::Confusion());
300+
301+
EXPECT_TRUE(isAdded) << "Should successfully prepend curve";
302+
303+
Handle(Geom_BSplineCurve) aResult = aConcat.BSplineCurve();
304+
ASSERT_FALSE(aResult.IsNull()) << "Result curve should not be null";
305+
306+
// Verify endpoints
307+
const gp_Pnt aStart = aResult->StartPoint();
308+
const gp_Pnt aEnd = aResult->EndPoint();
309+
310+
EXPECT_NEAR(aStart.Distance(gp_Pnt(0., 0., 0.)), 0., Precision::Confusion());
311+
EXPECT_NEAR(aEnd.Distance(gp_Pnt(6., 0., 0.)), 0., Precision::Confusion());
312+
}
313+
314+
//==================================================================================================
315+
316+
TEST(GeomConvert_CompCurveToBSplineCurveTest, EmptyInitialCurve)
317+
{
318+
// Test adding to an empty converter.
319+
GeomConvert_CompCurveToBSplineCurve aConcat;
320+
321+
TColgp_Array1OfPnt aPoles(1, 4);
322+
aPoles(1) = gp_Pnt(0., 0., 0.);
323+
aPoles(2) = gp_Pnt(1., 1., 0.);
324+
aPoles(3) = gp_Pnt(2., 1., 0.);
325+
aPoles(4) = gp_Pnt(3., 0., 0.);
326+
327+
TColStd_Array1OfReal aKnots(1, 2);
328+
aKnots(1) = 0.;
329+
aKnots(2) = 1.;
330+
331+
TColStd_Array1OfInteger aMults(1, 2);
332+
aMults(1) = 4;
333+
aMults(2) = 4;
334+
335+
Handle(Geom_BSplineCurve) aCurve = new Geom_BSplineCurve(aPoles, aKnots, aMults, 3);
336+
337+
const bool isAdded = aConcat.Add(aCurve, Precision::Confusion());
338+
339+
EXPECT_TRUE(isAdded) << "Should successfully add to empty converter";
340+
341+
Handle(Geom_BSplineCurve) aResult = aConcat.BSplineCurve();
342+
ASSERT_FALSE(aResult.IsNull()) << "Result curve should not be null";
343+
}
344+
345+
//==================================================================================================
346+
347+
TEST(GeomConvert_CompCurveToBSplineCurveTest, ClearAndReuse)
348+
{
349+
// Test Clear() method.
350+
TColgp_Array1OfPnt aPoles(1, 4);
351+
aPoles(1) = gp_Pnt(0., 0., 0.);
352+
aPoles(2) = gp_Pnt(1., 1., 0.);
353+
aPoles(3) = gp_Pnt(2., 1., 0.);
354+
aPoles(4) = gp_Pnt(3., 0., 0.);
355+
356+
TColStd_Array1OfReal aKnots(1, 2);
357+
aKnots(1) = 0.;
358+
aKnots(2) = 1.;
359+
360+
TColStd_Array1OfInteger aMults(1, 2);
361+
aMults(1) = 4;
362+
aMults(2) = 4;
363+
364+
Handle(Geom_BSplineCurve) aCurve = new Geom_BSplineCurve(aPoles, aKnots, aMults, 3);
365+
366+
GeomConvert_CompCurveToBSplineCurve aConcat(aCurve);
367+
368+
// Verify curve exists
369+
ASSERT_FALSE(aConcat.BSplineCurve().IsNull());
370+
371+
// Clear
372+
aConcat.Clear();
373+
EXPECT_TRUE(aConcat.BSplineCurve().IsNull()) << "Curve should be null after Clear()";
374+
375+
// Re-add
376+
const bool isAdded = aConcat.Add(aCurve, Precision::Confusion());
377+
EXPECT_TRUE(isAdded) << "Should successfully add after Clear()";
378+
EXPECT_FALSE(aConcat.BSplineCurve().IsNull()) << "Curve should exist after re-adding";
379+
}

0 commit comments

Comments
 (0)