Skip to content

Commit e4fda1f

Browse files
committed
MinimumCircleCylinder minor robustness improvement and unit tests
1 parent 07f02b1 commit e4fda1f

2 files changed

Lines changed: 59 additions & 20 deletions

File tree

‎TessellationAndVoxelizationGeometryLibrary/Enclosure Operations/MinimumCircleCylinder.cs‎

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,8 @@ public static Circle MinimumCircle(this IEnumerable<Vector2> pointsInput)
7070
for (int i = startIndex; i < numPoints; i++)
7171
{
7272
var dist = (points[i] - circle.Center).LengthSquared();
73-
if (dist > maxDistSqared)
73+
if (dist.IsGreaterThanNonNegligible(maxDistSqared,
74+
Math.Max(dist, maxDistSqared) * Constants.DefaultEqualityTolerance))
7475
{
7576
//Set max distance ONLY AFTER handling the stall counter logic.
7677
maxDistSqared = dist;
@@ -101,15 +102,28 @@ public static Circle MinimumCircle(this IEnumerable<Vector2> pointsInput)
101102
} while (newPointFoundOutsideCircle && totalIterationCounter < maxIterations);
102103
if (totalIterationCounter == maxIterations)
103104
{
104-
var filePathOut = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "cvxpoints.csv");
105-
OutputServices.Logger.LogError("MinimumCircle function reach max iteration counter: " + maxIterations);
106-
OutputServices.Logger.LogInformation("points saved to: " + filePathOut);
107-
File.WriteAllLines(filePathOut, pointsInput.Select(p => p.X + "," + p.Y));
108-
//Presenter.ShowAndHang(points, plot2DType: Plot2DType.Scatter);
105+
OutputServices.Logger.LogError("MinimumCircle reached its iteration limit {IterationLimit}.", maxIterations);
109106
}
110107

111-
return circle;
108+
// Boundary comparisons allow floating-point roundoff; the returned enclosure must still contain every input.
109+
var enclosingRadiusSquared = points.Max(point => (point - circle.Center).LengthSquared());
110+
return new Circle(circle.Center, Math.Max(circle.RadiusSquared, enclosingRadiusSquared));
112111
}
112+
113+
/// <summary>
114+
/// Determines whether a point is within a circle, allowing a scale-relative tolerance for
115+
/// floating-point roundoff at the boundary.
116+
/// </summary>
117+
/// <param name="circle">The circle to test.</param>
118+
/// <param name="point">The point to test.</param>
119+
/// <returns><see langword="true"/> when the point is inside or negligibly outside the circle.</returns>
120+
private static bool ContainsCirclePoint(Circle circle, Vector2 point)
121+
{
122+
var distanceSquared = (point - circle.Center).LengthSquared();
123+
return distanceSquared <= circle.RadiusSquared || distanceSquared.IsPracticallySame(circle.RadiusSquared,
124+
Math.Max(distanceSquared, circle.RadiusSquared) * Constants.DefaultEqualityTolerance);
125+
}
126+
113127
private static Circle FirstCircle(Vector2[] points)
114128
{
115129
// during the main loop, the most outside point will be moved to the front
@@ -120,18 +134,18 @@ private static Circle FirstCircle(Vector2[] points)
120134
// permutations in this function. This will ensure that FindCircle runs faster (without
121135
// extra conditions and ensures that it won't miss a case
122136
var circle = Circle.CreateFrom2Points(points[0], points[1]);
123-
if ((points[2] - circle.Center).LengthSquared() <= circle.RadiusSquared)
137+
if (ContainsCirclePoint(circle, points[2]))
124138
return circle;
125139
circle = Circle.CreateFrom2Points(points[0], points[2]);
126-
if ((points[1] - circle.Center).LengthSquared() <= circle.RadiusSquared)
140+
if (ContainsCirclePoint(circle, points[1]))
127141
{
128142
// since 0 and 2 are furthest apart, we need to swap 1 and 2
129143
// so that the two points in the circle are at the beinning of the list
130144
Global.SwapItemsInList(1, 2, points);
131145
return circle;
132146
}
133147
circle = Circle.CreateFrom2Points(points[1], points[2]);
134-
if ((points[0] - circle.Center).LengthSquared() <= circle.RadiusSquared)
148+
if (ContainsCirclePoint(circle, points[0]))
135149
{
136150
// since 1 and 2 are furthest apart, we need to swap 0 and 2
137151
// so that the two points in the circle are at the beinning of the list
@@ -157,22 +171,22 @@ private static Circle FindCircle(Vector2[] points)
157171

158172
// 1. make the 0-1 circle and check with 2 & 3
159173
var circle = Circle.CreateFrom2Points(points[0], points[1]);
160-
if ((points[2] - circle.Center).LengthSquared() <= circle.RadiusSquared
161-
&& (points[3] - circle.Center).LengthSquared() <= circle.RadiusSquared)
174+
if (ContainsCirclePoint(circle, points[2])
175+
&& ContainsCirclePoint(circle, points[3]))
162176
return circle;
163177

164178
// 2. make the 0-2 circle and check with 1 & 3
165179
circle = Circle.CreateFrom2Points(points[0], points[2]);
166-
if ((points[1] - circle.Center).LengthSquared() <= circle.RadiusSquared
167-
&& (points[3] - circle.Center).LengthSquared() <= circle.RadiusSquared)
180+
if (ContainsCirclePoint(circle, points[1])
181+
&& ContainsCirclePoint(circle, points[3]))
168182
{
169183
Global.SwapItemsInList(1, 2, points);
170184
return circle;
171185
}
172186
// 3. make the 0-3 circle and check with 1 & 2
173187
circle = Circle.CreateFrom2Points(points[0], points[3]);
174-
if ((points[1] - circle.Center).LengthSquared() <= circle.RadiusSquared
175-
&& (points[2] - circle.Center).LengthSquared() <= circle.RadiusSquared)
188+
if (ContainsCirclePoint(circle, points[1])
189+
&& ContainsCirclePoint(circle, points[2]))
176190
{
177191
Global.SwapItemsInList(1, 3, points);
178192
return circle;
@@ -183,15 +197,15 @@ private static Circle FindCircle(Vector2[] points)
183197
// circle 0-1-2
184198
var minRadiusSqd = double.PositiveInfinity;
185199
if (Circle.CreateFrom3Points(points[0], points[1], points[2], out tempCircle)
186-
&& (points[3] - tempCircle.Center).LengthSquared() <= tempCircle.RadiusSquared)
200+
&& ContainsCirclePoint(tempCircle, points[3]))
187201
{
188202
circle = tempCircle;
189203
minRadiusSqd = circle.RadiusSquared;
190204
}
191205
// circle 0-1-3
192206
var swap3And2 = false;
193207
if (Circle.CreateFrom3Points(points[0], points[1], points[3], out tempCircle)
194-
&& (points[2] - tempCircle.Center).LengthSquared() <= tempCircle.RadiusSquared
208+
&& ContainsCirclePoint(tempCircle, points[2])
195209
&& tempCircle.RadiusSquared < minRadiusSqd)
196210
{
197211
swap3And2 = true;
@@ -201,7 +215,7 @@ private static Circle FindCircle(Vector2[] points)
201215
// circle 0-2-3
202216
var swap3And1 = false;
203217
if (Circle.CreateFrom3Points(points[0], points[2], points[3], out tempCircle)
204-
&& (points[1] - tempCircle.Center).LengthSquared() <= tempCircle.RadiusSquared
218+
&& ContainsCirclePoint(tempCircle, points[1])
205219
&& tempCircle.RadiusSquared < minRadiusSqd)
206220
{
207221
swap3And1 = true;
@@ -535,4 +549,4 @@ public static Vector3 MinimumBoundingCylinderAxis<T>(this IEnumerable<T> vertice
535549
return likelyAxis;
536550
}
537551
}
538-
}
552+
}

‎Testing/PointCloud/MinimumCircleTesting.cs‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Collections.Generic;
33
using System.IO;
44
using TVGL;
5+
using Xunit;
56

67
namespace TVGLUnitTestsAndBenchmarking
78
{
@@ -10,6 +11,30 @@ public class MinimumCircleTesting
1011
static Random r = new Random();
1112
static double r100 => 200.0 * r.NextDouble() - 100.0;
1213

14+
[Fact]
15+
public void MinimumCircleExpandsForToleratedBoundaryRoundoff()
16+
{
17+
const double radius = 1_000_000.0;
18+
var center = new Vector2(100_000_000.0, -100_000_000.0);
19+
var nominalTop = center.Y + radius;
20+
var points = new[]
21+
{
22+
center + new Vector2(-radius, 0.0),
23+
center + new Vector2(radius, 0.0),
24+
new Vector2(center.X, Math.BitIncrement(nominalTop))
25+
};
26+
27+
var circle = points.MinimumCircle();
28+
var topDistanceSquared = (points[2] - circle.Center).LengthSquared();
29+
30+
Assert.True(topDistanceSquared > radius * radius,
31+
"The test point must be marginally outside the nominal two-point circle.");
32+
Assert.True(topDistanceSquared <= circle.RadiusSquared,
33+
"The returned circle must enclose a point accepted by the boundary tolerance.");
34+
Assert.Equal(center.X, circle.Center.X);
35+
Assert.Equal(center.Y, circle.Center.Y);
36+
}
37+
1338
internal static void Test1(int dataSize, int numTests)
1439
{
1540
// so the answer should be a circle of radius 101 centered at origin

0 commit comments

Comments
 (0)