• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

DomCR / ACadSharp / 19269332934

11 Nov 2025 02:49PM UTC coverage: 77.977% (+0.07%) from 77.909%
19269332934

Pull #868

github

web-flow
Merge 501626ef7 into 3305c3304
Pull Request #868: Spline refactor

6995 of 9781 branches covered (71.52%)

Branch coverage included in aggregate %.

175 of 197 new or added lines in 1 file covered. (88.83%)

17 existing lines in 3 files now uncovered.

26964 of 33769 relevant lines covered (79.85%)

101595.5 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

68.55
/src/ACadSharp/Entities/Spline.cs
1
using ACadSharp.Attributes;
2
using CSMath;
3
using CSUtilities.Extensions;
4
using System;
5
using System.Collections.Generic;
6
using System.Linq;
7

8
namespace ACadSharp.Entities
9
{
10
        /// <summary>
11
        /// Represents a <see cref="Spline"/> entity.
12
        /// </summary>
13
        /// <remarks>
14
        /// Object name <see cref="DxfFileToken.EntitySpline"/> <br/>
15
        /// Dxf class name <see cref="DxfSubclassMarker.Spline"/>
16
        /// </remarks>
17
        [DxfName(DxfFileToken.EntitySpline)]
18
        [DxfSubClass(DxfSubclassMarker.Spline)]
19
        public class Spline : Entity
20
        {
21
                /// <summary>
22
                /// Number of control points (in WCS).
23
                /// </summary>
24
                [DxfCodeValue(DxfReferenceType.Count, 73)]
25
                [DxfCollectionCodeValue(10, 20, 30)]
26
                public List<XYZ> ControlPoints { get; private set; } = new List<XYZ>();
15,432✔
27

28
                /// <summary>
29
                /// Control-point tolerance.
30
                /// </summary>
31
                [DxfCodeValue(43)]
32
                public double ControlPointTolerance { get; set; } = 0.0000001;
1,558✔
33

34
                /// <summary>
35
                /// Gets or sets the polynomial degree of the resulting spline.
36
                /// </summary>
37
                /// <remarks>
38
                /// Valid values are 1 (linear), degree 2 (quadratic), degree 3 (cubic), and so on up to degree 10.
39
                /// </remarks>
40
                [DxfCodeValue(71)]
41
                public int Degree { get; set; } = 3;
4,630✔
42

43
                /// <summary>
44
                /// End tangent—may be omitted in WCS.
45
                /// </summary>
46
                [DxfCodeValue(13, 23, 33)]
47
                public XYZ EndTangent { get; set; }
114✔
48

49
                /// <summary>
50
                /// Number of fit points (in WCS).
51
                /// </summary>
52
                [DxfCodeValue(DxfReferenceType.Count, 74)]
53
                [DxfCollectionCodeValue(11, 21, 31)]
54
                public List<XYZ> FitPoints { get; private set; } = new List<XYZ>();
1,074✔
55

56
                /// <summary>
57
                /// Fit tolerance.
58
                /// </summary>
59
                [DxfCodeValue(44)]
60
                public double FitTolerance { get; set; } = 0.0000000001;
889✔
61

62
                /// <summary>
63
                /// Spline flags.
64
                /// </summary>
65
                [DxfCodeValue(70)]
66
                public SplineFlags Flags { get => this._flags; set => this._flags = value; }
734✔
67

68
                /// <summary>
69
                /// Spline flags1.
70
                /// </summary>
71
                /// <remarks>
72
                /// Only valid for dwg.
73
                /// </remarks>
74
                public SplineFlags1 Flags1 { get => this._flags1; set => this._flags1 = value; }
207✔
75

76
                /// <summary>
77
                /// Gets a value indicating whether the knot vector has a valid count based on the control points, degree, and closure
78
                /// status of the curve.
79
                /// </summary>
80
                /// <remarks>The expected knot count is calculated as the number of control points plus the degree of the
81
                /// curve, adjusted for whether the curve is closed.</remarks>
82
                public bool HasValidKnotCount
83
                {
84
                        get
85
                        {
11✔
86
                                var expected = this.ControlPoints.Count + (this.IsClosed ? 2 : 1) * this.Degree + 1;
11!
87
                                return this.Knots.Count == expected;
11✔
88
                        }
11✔
89
                }
90

91
                /// <summary>
92
                /// Flag whether the spline is closed.
93
                /// </summary>
94
                public bool IsClosed
95
                {
96
                        get
97
                        {
57✔
98
                                return this.Flags.HasFlag(SplineFlags.Closed);
57✔
99
                        }
57✔
100
                        set
101
                        {
83✔
102
                                if (value)
83✔
103
                                {
2✔
104
                                        this._flags.AddFlag(SplineFlags.Closed);
2✔
105
                                        this._flags1.AddFlag(SplineFlags1.Closed);
2✔
106
                                }
2✔
107
                                else
108
                                {
81✔
109
                                        this._flags.RemoveFlag(SplineFlags.Closed);
81✔
110
                                        this._flags1.RemoveFlag(SplineFlags1.Closed);
81✔
111
                                }
81✔
112
                        }
83✔
113
                }
114

115
                /// <summary>
116
                /// Gets or sets a value indicating whether the spline is periodic.
117
                /// </summary>
118
                /// <remarks>A periodic spline forms a closed loop, where the start and end points are connected seamlessly.
119
                /// Setting this property updates the internal flags to reflect the periodicity of the spline.</remarks>
120
                public bool IsPeriodic
121
                {
122
                        get
123
                        {
55✔
124
                                return this.Flags.HasFlag(SplineFlags.Periodic);
55✔
125
                        }
55✔
126
                        set
127
                        {
4✔
128
                                if (value)
4!
129
                                {
×
130
                                        this._flags.AddFlag(SplineFlags.Periodic);
×
131
                                }
×
132
                                else
133
                                {
4✔
134
                                        this._flags.RemoveFlag(SplineFlags.Periodic);
4✔
135
                                }
4✔
136
                        }
4✔
137
                }
138

139
                /// <summary>
140
                /// Knot parameters.
141
                /// </summary>
142
                public KnotParametrization KnotParametrization { get; set; }
378✔
143

144
                /// <summary>
145
                /// Number of knots.
146
                /// </summary>
147
                [DxfCodeValue(DxfReferenceType.Count, 72)]
148
                [DxfCollectionCodeValue(40)]
149
                public List<double> Knots { get; private set; } = new List<double>();
6,720✔
150

151
                /// <summary>
152
                /// Knot tolerance.
153
                /// </summary>
154
                [DxfCodeValue(42)]
155
                public double KnotTolerance { get; set; } = 0.0000001;
1,558✔
156

157
                /// <summary>
158
                /// Specifies the three-dimensional normal unit vector for the object.
159
                /// </summary>
160
                /// <remarks>
161
                /// Omitted if the spline is non-planar.
162
                /// </remarks>
163
                [DxfCodeValue(210, 220, 230)]
164
                public XYZ Normal { get; set; } = XYZ.AxisZ;
3,688✔
165

166
                /// <inheritdoc/>
167
                public override string ObjectName => DxfFileToken.EntitySpline;
2,680✔
168

169
                /// <inheritdoc/>
170
                public override ObjectType ObjectType => ObjectType.SPLINE;
18✔
171

172
                /// <summary>
173
                /// Start tangent—may be omitted in WCS.
174
                /// </summary>
175
                [DxfCodeValue(12, 22, 32)]
176
                public XYZ StartTangent { get; set; }
125✔
177

178
                /// <inheritdoc/>
179
                public override string SubclassMarker => DxfSubclassMarker.Spline;
5,763✔
180

181
                /// <summary>
182
                /// Weight(if not 1); with multiple group pairs, they are present if all are not 1.
183
                /// </summary>
184
                [DxfCodeValue(DxfReferenceType.Count, 41)]
185
                public List<double> Weights { get; private set; } = new List<double>();
892✔
186

187
                public const short MaxDegree = 10;
188

189
                private SplineFlags _flags;
190

191
                private SplineFlags1 _flags1;
192

193
                /// <inheritdoc/>
194
                public Spline() : base() { }
2,340✔
195

196
                /// <inheritdoc/>
197
                public override void ApplyTransform(Transform transform)
198
                {
×
199
                        this.Normal = this.transformNormal(transform, this.Normal);
×
200

201
                        for (int i = 0; i < this.ControlPoints.Count; i++)
×
202
                        {
×
203
                                this.ControlPoints[i] = transform.ApplyTransform(this.ControlPoints[i]);
×
204
                        }
×
205

206
                        for (int i = 0; i < this.FitPoints.Count; i++)
×
207
                        {
×
208
                                this.FitPoints[i] = transform.ApplyTransform(this.FitPoints[i]);
×
209
                        }
×
210
                }
×
211

212
                /// <inheritdoc/>
213
                public override CadObject Clone()
214
                {
3✔
215
                        Spline clone = (Spline)base.Clone();
3✔
216

217
                        clone.ControlPoints = new List<XYZ>(this.ControlPoints);
3✔
218
                        clone.FitPoints = new List<XYZ>(this.FitPoints);
3✔
219
                        clone.Weights = new List<double>(this.Weights);
3✔
220
                        clone.Knots = new List<double>(this.Knots);
3✔
221

222
                        return clone;
3✔
223
                }
3✔
224

225
                /// <inheritdoc/>
226
                public override BoundingBox GetBoundingBox()
227
                {
1✔
228
                        List<XYZ> vertices;
229
                        if (!this.TryPolygonalVertexes(256, out vertices))
1!
NEW
230
                        {
×
NEW
231
                                vertices = new List<XYZ>(this.FitPoints);
×
NEW
232
                        }
×
233

234
                        return BoundingBox.FromPoints(vertices);
1✔
235
                }
1✔
236

237
                /// <summary>
238
                /// Calculates the point on the spline at the specified parametric position.
239
                /// </summary>
240
                /// <param name="t">The parametric position along the spline, where <see langword="0"/> represents the start of the spline and <see
241
                /// langword="1"/> represents the end. Must be a value between <see langword="0"/> and <see langword="1"/>.</param>
242
                /// <returns>The point on the spline at the specified parametric position, represented as an <see cref="XYZ"/> object.</returns>
243
                /// <exception cref="ArgumentOutOfRangeException">Thrown if <paramref name="t"/> is less than <see langword="0"/> or greater than <see langword="1"/>.</exception>
244
                public XYZ PointOnSpline(double t)
245
                {
×
246
                        if (t < 0 || t > 1)
×
247
                        {
×
248
                                throw new ArgumentOutOfRangeException(nameof(t), t, "The parametric position must be a value between 0 and 1.");
×
249
                        }
250

251
                        if (t == 1)
×
252
                        {
×
253
                                t -= double.Epsilon;
×
254
                        }
×
255

NEW
256
                        this.prepare(out XYZ[] controlPts, out double[] weights, out double[] knots);
×
UNCOV
257
                        this.getStartAndEndKnots(knots, out double uStart, out double uEnd);
×
258

259
                        double uDelta = (uEnd - uStart) * t;
×
260
                        double u = uStart + uDelta;
×
261
                        return c(controlPts, weights, knots, this.Degree, u);
×
262
                }
×
263

264
                /// <summary>
265
                /// Generates a list of vertex points representing a polygonal approximation of the curve.
266
                /// </summary>
267
                /// <remarks>The method calculates the vertex points by dividing the curve into segments based on the specified
268
                /// precision. If the curve is not closed or periodic, the last control point is explicitly added to the
269
                /// result.</remarks>
270
                /// <param name="precision">The number of segments used to approximate the curve. Must be equal to or greater than 2.</param>
271
                /// <returns>A list of <see cref="XYZ"/> objects representing the vertex points of the polygonal approximation.</returns>
272
                /// <exception cref="ArgumentOutOfRangeException">Thrown if <paramref name="precision"/> is less than 2.</exception>
273
                public List<XYZ> PolygonalVertexes(int precision)
274
                {
11✔
275
                        if (precision < 2)
11!
276
                        {
×
277
                                throw new ArgumentOutOfRangeException(nameof(precision), precision, "The precision must be equal or greater than two.");
×
278
                        }
279

280
                        List<XYZ> vertexes = new List<XYZ>();
11✔
281

282
                        this.prepare(out XYZ[] controlPts, out double[] weights, out double[] knots);
11✔
283
                        this.getStartAndEndKnots(knots, out double uStart, out double uEnd);
11✔
284

285
                        if (!this.IsClosed && !this.IsPeriodic)
11!
286
                        {
11✔
287
                                precision -= 1;
11✔
288
                        }
11✔
289

290
                        double uDelta = (uEnd - uStart) / precision;
11✔
291

292
                        for (int i = 0; i < precision; i++)
5,614✔
293
                        {
2,796✔
294
                                double u = uStart + uDelta * i;
2,796✔
295
                                vertexes.Add(c(controlPts, weights, knots, this.Degree, u));
2,796✔
296
                        }
2,796✔
297

298
                        if (!(this.IsClosed || this.IsPeriodic))
11!
299
                        {
11✔
300
                                vertexes.Add(controlPts[controlPts.Length - 1]);
11✔
301
                        }
11✔
302

303
                        return vertexes;
11✔
304
                }
11✔
305

306
                /// <summary>
307
                /// Attempts to calculate a point on the spline at the specified parameter value.
308
                /// </summary>
309
                /// <remarks>This method catches any exceptions that occur during the calculation and returns <see
310
                /// langword="false"/> in such cases. Ensure that the parameter <paramref name="t"/> is within the valid range for the
311
                /// spline to avoid errors.</remarks>
312
                /// <param name="t">The parameter value along the spline, typically in the range [0, 1].</param>
313
                /// <param name="point">When this method returns, contains the calculated point on the spline if the operation succeeds; otherwise, <see
314
                /// cref="XYZ.NaN"/>. This parameter is passed uninitialized.</param>
315
                /// <returns><see langword="true"/> if the point was successfully calculated; otherwise, <see langword="false"/>.</returns>
316
                public bool TryPointOnSpline(double t, out XYZ point)
317
                {
×
318
                        point = XYZ.NaN;
×
319

320
                        try
321
                        {
×
322
                                point = this.PointOnSpline(t);
×
323
                                return true;
×
324
                        }
325
                        catch (Exception)
×
326
                        {
×
327
                                return false;
×
328
                        }
329
                }
×
330

331
                /// <summary>
332
                /// Attempts to calculate the polygonal vertexes of the current object with the specified precision.
333
                /// </summary>
334
                /// <remarks>This method catches and suppresses any exceptions that occur during the calculation, returning
335
                /// <see langword="false"/> in such cases. The <paramref name="points"/> parameter will always be initialized, even if
336
                /// the operation fails.</remarks>
337
                /// <param name="precision">The level of precision to use when calculating the polygonal vertexes. Must be a positive integer.</param>
338
                /// <param name="points">When this method returns, contains a list of <see cref="XYZ"/> objects representing the calculated polygonal
339
                /// vertexes, if the operation succeeds; otherwise, contains an empty list.</param>
340
                /// <returns><see langword="true"/> if the polygonal vertexes were successfully calculated; otherwise, <see langword="false"/>.</returns>
341
                public bool TryPolygonalVertexes(int precision, out List<XYZ> points)
342
                {
1✔
343
                        points = new List<XYZ>();
1✔
344

345
                        try
346
                        {
1✔
347
                                points = this.PolygonalVertexes(precision);
1✔
348
                                return true;
1✔
349
                        }
350
                        catch (Exception)
×
351
                        {
×
352
                                return false;
×
353
                        }
354
                }
1✔
355

356
                /// <summary>
357
                /// Updates the spline's control points, knots, and weights based on the current fit points and tangents.
358
                /// </summary>
359
                /// <remarks>This method performs the following operations: <list type="bullet"> <item>Validates the spline's
360
                /// degree, fit points, and knot parametrization.</item> <item>Generates knots and control points based on the fit
361
                /// points and tangents.</item> <item>Refines the tangents iteratively if necessary, up to the specified iteration
362
                /// limit.</item> <item>Assigns uniform weights to the control points.</item> </list> The method returns <see
363
                /// langword="false"/> if the spline's degree is not 3, if there are fewer than two fit points, if the knot
364
                /// parametrization is set to <see cref="KnotParametrization.Custom"/>, or if tangent refinement fails.</remarks>
365
                /// <param name="iterationLimit">The maximum number of iterations allowed for refining the tangents. Defaults to <see cref="byte.MaxValue"/>.</param>
366
                /// <returns><see langword="true"/> if the spline was successfully updated; otherwise, <see langword="false"/>.</returns>
367
                public bool UpdateFromFitPoints(uint iterationLimit = byte.MaxValue)
368
                {
12✔
369
                        if (this.Degree != 3
12!
370
                                || this.FitPoints.Count < 2
12✔
371
                                || this.KnotParametrization == KnotParametrization.Custom)
12✔
372
                        {
×
373
                                return false;
×
374
                        }
375

376
                        if (this.FitPoints.Count == 2)
12✔
377
                        {
1✔
378
                                this.straightLine();
1✔
379
                                return true;
1✔
380
                        }
381

382
                        this.Knots.Clear();
11✔
383
                        this.ControlPoints.Clear();
11✔
384
                        this.Weights.Clear();
11✔
385

386
                        //Compute knots
387
                        var fitPoints = this.FitPoints.ToArray();
11✔
388
                        double[] knotValues = generateKnotValues(this.KnotParametrization, fitPoints);
11✔
389
                        this.Knots = addSideKnots(this.Degree, knotValues);
11✔
390

391
                        if (this.StartTangent.IsEqual(XYZ.Zero) || this.EndTangent.IsEqual(XYZ.Zero))
11!
392
                        {
11✔
393
                                //https://dccg.upc.edu/wp-content/uploads/2025/05/3.3-Spline-Interpolation.pdf
394
                                //Try to manually get the first and last tangent
395
                                take3(knotValues, false, out double fisrtK1, out double firstK2, out double firstK3);
11✔
396
                                take3(knotValues, true, out double lastK1, out double lastK2, out double lastK3);
11✔
397

398
                                double startScale = 3.0 / (firstK2 - fisrtK1);
11✔
399
                                double endScale = 3.0 / (lastK1 - lastK2);
11✔
400

401
                                take3(fitPoints, false, out XYZ fitPt1, out XYZ fitPt2, out XYZ fitPt3);
11✔
402
                                XYZ startTangent = startScale * (fitPt2 - fitPt1 - 0.5 * (fitPt3 - fitPt2));
11✔
403
                                take3(fitPoints, true, out fitPt1, out fitPt2, out fitPt3);
11✔
404
                                XYZ endTangent = endScale * (fitPt1 - fitPt2 - 0.5 * (fitPt2 - fitPt3));
11✔
405

406
                                double startMag = 1.0 / startTangent.ToEnumerable().Sum(v => v * v);
44✔
407
                                double endMag = 1.0 / endTangent.ToEnumerable().Sum(v => v * v);
44✔
408
                                if (double.IsInfinity(startMag) || double.IsInfinity(endMag))
11!
NEW
409
                                {
×
410
                                        //Found this error in some cases
NEW
411
                                        return false;
×
412
                                }
413

414
                                double startDiv = (firstK2 + firstK3 - 2.0 * fisrtK1) / (firstK2 - fisrtK1);
11✔
415
                                double endDiv = (2.0 * lastK1 - lastK2 - lastK3) / (lastK1 - lastK2);
11✔
416

417
                                double startDelta = double.MaxValue;
11✔
418
                                double endDelta = double.MaxValue;
11✔
419

420
                                XYZ[] controlPoints = null;
11✔
421
                                var knots = this.Knots.ToArray();
11✔
422
                                do
423
                                {
187✔
424
                                        XYZ lastV1 = startTangent;
187✔
425
                                        XYZ lastV2 = endTangent;
187✔
426
                                        controlPoints = computeControlPoints(this.Degree, knots, fitPoints, startTangent, endTangent);
187✔
427

428
                                        // Update tangents based on new control points, only works for degree 3
429
                                        take3(controlPoints, false, out XYZ pt1, out XYZ pt2, out XYZ pt3);
187✔
430
                                        startTangent = 0.5 * (pt2 - pt1 + (pt3 - pt1) / startDiv) * startScale;
187✔
431

432
                                        double currStartDelta = startDelta;
187✔
433
                                        startDelta = startMag * (startTangent - lastV1).ToEnumerable().Sum(v => v * v);
748✔
434

435
                                        take3(controlPoints, true, out pt1, out pt2, out pt3);
187✔
436
                                        endTangent = 0.5 * (pt1 - pt2 + (pt1 - pt3) / endDiv) * endScale;
187✔
437

438
                                        double currEndDelta = endDelta;
187✔
439
                                        endDelta = endMag * (endTangent - lastV2).ToEnumerable().Sum(v => v * v);
748✔
440

441
                                        if ((startDelta >= currStartDelta && endDelta >= currEndDelta)
187!
442
                                                || --iterationLimit <= 0)
187✔
NEW
443
                                        {
×
NEW
444
                                                return false;
×
445
                                        }
446
                                }
187✔
447
                                while (startDelta + endDelta > MathHelper.Epsilon);
187✔
448

449
                                this.ControlPoints.AddRange(controlPoints);
11✔
450
                        }
11✔
451
                        else
UNCOV
452
                        {
×
NEW
453
                                this.ControlPoints.AddRange(computeControlPoints(this.Degree, this.Knots.ToArray(), fitPoints, this.StartTangent, this.EndTangent));
×
UNCOV
454
                        }
×
455

456
                        this.Weights = Enumerable.Repeat(1.0d, this.ControlPoints.Count).ToList();
11✔
457

458
                        return true;
11✔
459
                }
12✔
460

461
                private static List<double> addSideKnots(int degree, double[] knots)
462
                {
12✔
463
                        List<double> list = new List<double>();
12✔
464
                        for (int i = 0; i < degree; i++)
96✔
465
                        {
36✔
466
                                list.Add(knots[0]);
36✔
467
                        }
36✔
468

469
                        list.AddRange(knots);
12✔
470

471
                        double item = list[list.Count - 1];
12✔
472
                        for (int j = 0; j < degree; j++)
96✔
473
                        {
36✔
474
                                list.Add(item);
36✔
475
                        }
36✔
476

477
                        return list;
12✔
478
                }
12✔
479

480
                private static XYZ c(XYZ[] ctrlPoints, double[] weights, double[] knots, int degree, double u)
481
                {
2,796✔
482
                        XYZ vectorSum = XYZ.Zero;
2,796✔
483
                        double denominatorSum = 0.0;
2,796✔
484

485
                        // optimization suggested by ThVoss
486
                        for (int i = 0; i < ctrlPoints.Length; i++)
30,510✔
487
                        {
12,459✔
488
                                double nurb = computeNurb(knots, i, degree, u);
12,459✔
489
                                denominatorSum += nurb * weights[i];
12,459✔
490
                                vectorSum += weights[i] * nurb * ctrlPoints[i];
12,459✔
491
                        }
12,459✔
492

493
                        // avoid possible divided by zero error, this should never happen
494
                        if (Math.Abs(denominatorSum) < double.Epsilon)
2,796✔
495
                        {
255✔
496
                                return XYZ.Zero;
255✔
497
                        }
498

499
                        return 1.0 / denominatorSum * vectorSum;
2,541✔
500
                }
2,796✔
501

502
                private static XYZ[] computeControlPoints(
503
                        int degree,
504
                        double[] knots,
505
                        IList<XYZ> fitPoints,
506
                        XYZ startTangent, XYZ endTangent)
507
                {
187✔
508
                        XYZ[] controlPoints = new XYZ[fitPoints.Count - 1 + degree];
187✔
509

510
                        // Set endpoints and tangent control points
511
                        controlPoints[0] = fitPoints[0];
187✔
512
                        controlPoints[1] = fitPoints[0] + startTangent * ((knots[4] - knots[3]) / 3.0);
187✔
513
                        controlPoints[fitPoints.Count + 1] = fitPoints[fitPoints.Count - 1];
187✔
514
                        controlPoints[fitPoints.Count] = fitPoints[fitPoints.Count - 1] - endTangent * ((knots[fitPoints.Count + 2] - knots[fitPoints.Count + 1]) / 3.0);
187✔
515

516
                        if (fitPoints.Count - 1 > 1)
187✔
517
                        {
187✔
518
                                var basis = new double[4];
187✔
519
                                var factors = new double[fitPoints.Count];
187✔
520

521
                                // Precompute basis for first interior point
522
                                evaluateBasisFunctions(degree, knots, 4, knots[4], basis);
187✔
523
                                double denom = basis[1];
187✔
524
                                controlPoints[2] = (fitPoints[1] - basis[0] * controlPoints[1]) / denom;
187✔
525

526
                                // Forward sweep for interior control points
527
                                for (int i = 3; i < fitPoints.Count - 1; i++)
712✔
528
                                {
169✔
529
                                        factors[i] = basis[2] / denom;
169✔
530
                                        evaluateBasisFunctions(degree, knots, i + 2, knots[i + 2], basis);
169✔
531
                                        denom = basis[1] - basis[0] * factors[i];
169✔
532
                                        controlPoints[i] = (fitPoints[i - 1] - basis[0] * controlPoints[i - 1]) / denom;
169✔
533
                                }
169✔
534

535
                                // Last interior control point
536
                                factors[fitPoints.Count - 1] = basis[2] / denom;
187✔
537
                                evaluateBasisFunctions(degree, knots, fitPoints.Count + 1, knots[fitPoints.Count + 1], basis);
187✔
538
                                double denomLast = basis[1];
187✔
539
                                double numLeft = basis[0];
187✔
540
                                double factorLast = factors[fitPoints.Count - 1];
187✔
541
                                controlPoints[fitPoints.Count - 1] = (fitPoints[fitPoints.Count - 2] - basis[2] * controlPoints[fitPoints.Count] - numLeft * controlPoints[fitPoints.Count - 2]) / (denomLast - numLeft * factorLast);
187✔
542

543
                                // Backward substitution for tridiagonal system
544
                                if (fitPoints.Count - 1 <= 2)
187✔
545
                                {
18✔
546
                                        controlPoints[fitPoints.Count - 1] *= 0.75;
18✔
547
                                }
18✔
548
                                else
549
                                {
169✔
550
                                        for (int j = fitPoints.Count - 2; j >= 2; j--)
1,014✔
551
                                        {
338✔
552
                                                controlPoints[j] -= factors[j + 1] * controlPoints[j + 1];
338✔
553
                                        }
338✔
554
                                }
169✔
555
                        }
187✔
556
                        return controlPoints;
187✔
557
                }
187✔
558

559
                private static double computeNurb(double[] knots, int i, int p, double u)
560
                {
186,885✔
561
                        if (p <= 0)
186,885✔
562
                        {
99,672✔
563
                                if (knots[i] <= u && u < knots[i + 1])
99,672✔
564
                                {
22,368✔
565
                                        return 1;
22,368✔
566
                                }
567

568
                                return 0.0;
77,304✔
569
                        }
570

571
                        double leftCoefficient = 0.0;
87,213✔
572
                        if (!(Math.Abs(knots[i + p] - knots[i]) < double.Epsilon))
87,213✔
573
                        {
39,681✔
574
                                leftCoefficient = (u - knots[i]) / (knots[i + p] - knots[i]);
39,681✔
575
                        }
39,681✔
576

577
                        double rightCoefficient = 0.0; // article contains error here, denominator is Knots[i + p + 1] - Knots[i + 1]
87,213✔
578
                        if (!(Math.Abs(knots[i + p + 1] - knots[i + 1]) < double.Epsilon))
87,213✔
579
                        {
39,681✔
580
                                rightCoefficient = (knots[i + p + 1] - u) / (knots[i + p + 1] - knots[i + 1]);
39,681✔
581
                        }
39,681✔
582

583
                        return leftCoefficient * computeNurb(knots, i, p - 1, u) + rightCoefficient * computeNurb(knots, i + 1, p - 1, u);
87,213✔
584
                }
186,885✔
585

586
                private static double[] createKnotVector(int numControlPoints, int degree, bool isPeriodic)
UNCOV
587
                {
×
588
                        // create knot vector
589
                        int numKnots;
590
                        double[] knots;
591

592
                        if (!isPeriodic)
×
593
                        {
×
594
                                numKnots = numControlPoints + degree + 1;
×
595
                                knots = new double[numKnots];
×
596

597
                                int i;
598
                                for (i = 0; i <= degree; i++)
×
599
                                {
×
600
                                        knots[i] = 0.0;
×
601
                                }
×
602

603
                                for (; i < numControlPoints; i++)
×
604
                                {
×
605
                                        knots[i] = i - degree;
×
606
                                }
×
607

608
                                for (; i < numKnots; i++)
×
609
                                {
×
610
                                        knots[i] = numControlPoints - degree;
×
611
                                }
×
612
                        }
×
613
                        else
614
                        {
×
615
                                numKnots = numControlPoints + 2 * degree + 1;
×
616
                                knots = new double[numKnots];
×
617

618
                                double factor = 1.0 / (numControlPoints - degree);
×
619
                                for (int i = 0; i < numKnots; i++)
×
620
                                {
×
621
                                        knots[i] = (i - degree) * factor;
×
622
                                }
×
623
                        }
×
624

625
                        return knots;
×
626
                }
×
627

628
                private static void evaluateBasisFunctions(int degree, double[] knots, int knotIndex, double u, double[] result)
629
                {
543✔
630
                        //https://www.itwinjs.org/reference/core-geometry/bspline/knotvector/evaluatebasisfunctions/
631
                        var leftBasis = new double[degree + 1];
543✔
632
                        var rightBasis = new double[degree + 1];
543✔
633

634
                        result[0] = 1.0;
543✔
635
                        for (int i = 0; i < degree; i++)
4,344✔
636
                        {
1,629✔
637
                                leftBasis[i] = u - knots[knotIndex - i];
1,629✔
638
                                rightBasis[i] = knots[knotIndex + i + 1] - u;
1,629✔
639
                                double carry = 0.0;
1,629✔
640
                                for (int j = 0; j <= i; j++)
9,774✔
641
                                {
3,258✔
642
                                        double fraction = result[j] / (rightBasis[j] + leftBasis[i - j]);
3,258✔
643
                                        result[j] = carry + rightBasis[j] * fraction;
3,258✔
644
                                        carry = leftBasis[i - j] * fraction;
3,258✔
645
                                }
3,258✔
646
                                result[i + 1] = carry;
1,629✔
647
                        }
1,629✔
648
                }
543✔
649

650
                private static double[] generateKnots(XYZ[] fitPoints, bool applySqrt)
651
                {
12✔
652
                        double[] list = new double[fitPoints.Length];
12✔
653
                        double init = 0.0;
12✔
654
                        XYZ pt = XYZ.NaN;
12✔
655
                        for (int i = 0; i < fitPoints.Length; i++)
134✔
656
                        {
55✔
657
                                XYZ fp = fitPoints[i];
55✔
658
                                if (!pt.IsNaN())
55✔
659
                                {
43✔
660
                                        if (applySqrt)
43!
NEW
661
                                        {
×
NEW
662
                                                init += System.Math.Sqrt((fp - pt).GetLength());
×
NEW
663
                                        }
×
664
                                        else
665
                                        {
43✔
666
                                                init += (fp - pt).GetLength();
43✔
667
                                        }
43✔
668
                                }
43✔
669

670
                                list[i] = init;
55✔
671
                                pt = fp;
55✔
672
                        }
55✔
673

674
                        return list;
12✔
675
                }
12✔
676

677
                private static double[] generateKnotsUniform(int fitPoints)
NEW
678
                {
×
NEW
679
                        double[] list = new double[fitPoints];
×
NEW
680
                        for (int i = 0; i < fitPoints; i++)
×
NEW
681
                        {
×
NEW
682
                                list[i] = i;
×
UNCOV
683
                        }
×
684

NEW
685
                        return list;
×
NEW
686
                }
×
687

688
                private static double[] generateKnotValues(KnotParametrization parametrization, XYZ[] fitPoints)
689
                {
12✔
690
                        switch (parametrization)
12!
691
                        {
692
                                case KnotParametrization.Chord:
693
                                        return generateKnots(fitPoints, false);
12✔
694
                                case KnotParametrization.SquareRoot:
NEW
695
                                        return generateKnots(fitPoints, true);
×
696
                                case KnotParametrization.Uniform:
NEW
697
                                        return generateKnotsUniform(fitPoints.Length);
×
698
                                //Custom cannot be processed
699
                                case KnotParametrization.Custom:
700
                                default:
NEW
701
                                        return [];
×
702
                        }
703
                }
12✔
704

705
                private static void take3<T>(T[] pts, bool reverse, out T pt1, out T pt2, out T pt3)
706
                                                                                                                                                                        where T : struct
707
                {
418✔
708
                        if (reverse)
418✔
709
                        {
209✔
710
                                pt1 = pts[pts.Length - 1];
209✔
711
                                pt2 = pts[pts.Length - 2];
209✔
712
                                pt3 = pts[pts.Length - 3];
209✔
713
                        }
209✔
714
                        else
715
                        {
209✔
716
                                pt1 = pts[0];
209✔
717
                                pt2 = pts[1];
209✔
718
                                pt3 = pts[2];
209✔
719
                        }
209✔
720
                }
418✔
721

722
                private void getStartAndEndKnots(double[] knots, out double uStart, out double uEnd)
723
                {
11✔
724
                        if (this.IsClosed)
11!
725
                        {
×
726
                                uStart = knots[0];
×
727
                                uEnd = knots[knots.Length - 1];
×
728
                        }
×
729
                        else if (this.IsPeriodic)
11!
730
                        {
×
731
                                uStart = knots[this.Degree];
×
732
                                uEnd = knots[knots.Length - this.Degree - 1];
×
733
                        }
×
734
                        else
735
                        {
11✔
736
                                uStart = knots[0];
11✔
737
                                uEnd = knots[knots.Length - 1];
11✔
738
                        }
11✔
739
                }
11✔
740

741
                private void prepare(out XYZ[] controlPts, out double[] weights, out double[] knots)
742
                {
11✔
743
                        var c = this.ControlPoints.ToArray();
11✔
744
                        var w = this.Weights.ToArray();
11✔
745
                        knots = this.Knots.ToArray();
11✔
746

747
                        // control points
748
                        int numCtrlPoints = c.Length;
11✔
749
                        if (numCtrlPoints == 0)
11!
750
                        {
×
751
                                throw new ArgumentException("A spline entity with control points is required.", nameof(c));
×
752
                        }
753

754
                        // weights
755
                        if (w.Length == 0 || w.Length != numCtrlPoints)
11✔
756
                        {
10✔
757
                                // give the default 1.0 to the control points weights
758
                                w = Enumerable.Repeat(1.0d, this.ControlPoints.Count).ToArray();
10✔
759
                        }
10✔
760

761
                        // knots
762
                        if (knots.Length == 0 || !this.HasValidKnotCount)
11!
763
                        {
×
764
                                knots = createKnotVector(numCtrlPoints, this.Degree, this.IsPeriodic);
×
765
                        }
×
766

767
                        if (this.IsPeriodic)
11!
768
                        {
×
769
                                controlPts = new XYZ[numCtrlPoints + this.Degree];
×
770
                                weights = new double[numCtrlPoints + this.Degree];
×
771
                                for (int i = 0; i < this.Degree; i++)
×
772
                                {
×
773
                                        int index = numCtrlPoints - this.Degree + i;
×
774
                                        controlPts[i] = c[index];
×
775
                                        weights[i] = w[index];
×
776
                                }
×
777

778
                                c.CopyTo(controlPts, this.Degree);
×
779
                                w.CopyTo(weights, this.Degree);
×
780
                        }
×
781
                        else
782
                        {
11✔
783
                                controlPts = c;
11✔
784
                                weights = w;
11✔
785
                        }
11✔
786

787
                        double uStart;
788
                        double uEnd;
789
                        List<XYZ> vertexes = new List<XYZ>();
11✔
790

791
                        if (this.IsClosed)
11!
792
                        {
×
793
                                uStart = knots[0];
×
794
                                uEnd = knots[knots.Length - 1];
×
795
                        }
×
796
                        else if (this.IsPeriodic)
11!
797
                        {
×
798
                                uStart = knots[this.Degree];
×
799
                                uEnd = knots[knots.Length - this.Degree - 1];
×
800
                        }
×
801
                        else
802
                        {
11✔
803
                                uStart = knots[0];
11✔
804
                                uEnd = knots[knots.Length - 1];
11✔
805
                        }
11✔
806
                }
11✔
807

808
                private void straightLine()
809
                {
1✔
810
                        // Special case: Bezier curve should be a straight line.
811
                        var fitPoints = this.FitPoints.ToArray();
1✔
812

813
                        this.Knots.Clear();
1✔
814
                        this.Knots.AddRange(addSideKnots(this.Degree, generateKnotValues(this.KnotParametrization, fitPoints)));
1✔
815

816
                        this.ControlPoints.Clear();
1✔
817
                        XYZ v = (fitPoints[1] - fitPoints[0]) / 3.0;
1✔
818
                        this.ControlPoints.Add(fitPoints[0]);
1✔
819
                        this.ControlPoints.Add(fitPoints[0] + v);
1✔
820
                        this.ControlPoints.Add(fitPoints[1] - v);
1✔
821
                        this.ControlPoints.Add(fitPoints[1]);
1✔
822

823
                        this.Weights.Clear();
1✔
824
                        this.Weights.AddRange([1, 1, 1, 1]);
1✔
825
                }
1✔
826
        }
827
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc