public static Circle2 FromThreePoints(Vector2 a, Vector2 b, Vector2 c) { // form the three chords var ab = LineSegment2.FromOriginAndDestination(a, b); var bc = LineSegment2.FromOriginAndDestination(b, c); var ca = LineSegment2.FromOriginAndDestination(c, a); // for numerical reasons, use the longest two var chords = new LineSegment2[] { ab, bc, ca }; chords = chords.OrderByDescending(chord => chord.SquaredLength).Take(2).ToArray(); // take the perpendicular bisectors of these two chords var p1 = chords[0].PerpendicularBisector; var p2 = chords[1].PerpendicularBisector; // where they intersect is the center var maybeCenter = Line2.Intersection(p1, p2); if (maybeCenter == null) { throw new IllConditionedProblemException(); } var center = maybeCenter.Value; // find the radius as the distance from one of the points to the center // for symmetry we could do all three and average // because we care about conservative inclusion, instead we will take the maximum var r = Math.Max(Vector2.DistanceBetweenPoints(center, a), Math.Max(Vector2.DistanceBetweenPoints(center, b), Vector2.DistanceBetweenPoints(center, c))); return(new Circle2(center, r)); }
public Circle2 ComputeMinimumCircumscribingCircle() { var n = _points.Count; if (n == 0) { return(new Circle2(Vector2.Zero, 0)); } else if (n == 1) { return(new Circle2(_points[0], 0)); } else if (n == 2) { var segment = LineSegment2.FromOriginAndDestination(_points[0], _points[1]); return(new Circle2(segment.Midpoint, segment.Length / 2)); } else if (n == 3) { return(Circle2.FromThreePoints(_points[0], _points[1], _points[2])); } else { // brute force! Circle2 incumbent = new Circle2(Vector2.Zero, double.PositiveInfinity); double incumbentRadius = double.PositiveInfinity; Circle2 incumbentMiss = new Circle2(Vector2.Zero, double.PositiveInfinity); double incumbentMissPenalty = double.PositiveInfinity; for (int i = 0; i < n - 2; i++) { for (int j = i + 1; j < n - 1; j++) { for (int k = j + 1; k < n; k++) { var candidate = Circle2.FromThreePoints(_points[i], _points[j], _points[k]); // the order of these tests is very important because one is much cheaper than the other if (candidate.Radius < incumbentRadius) { if (double.IsInfinity(incumbentRadius)) { // we have not yet found one that includes all points, so we might still need to update the incumbent miss var candidateMissPenalty = _points.Max(p => candidate.SignedDistanceFromCircle(p)); if (candidateMissPenalty <= 0) { // all points pass strict inclusion, this is the new incumbent incumbent = candidate; incumbentRadius = candidate.Radius; } if (double.IsInfinity(incumbentRadius) && candidateMissPenalty < incumbentMissPenalty) { // this one doesn't fit, but it's the best we've seen so far so we might need it if things go poorly numerically incumbentMiss = candidate; incumbentMissPenalty = candidateMissPenalty; } } else { // since we don't care about updating the miss, we can instead use short-circuiting while evaluating inclusion if (_points.All(p => candidate.Contains(p))) { incumbent = candidate; incumbentRadius = candidate.Radius; } } } } } } if (double.IsInfinity(incumbentRadius)) { // we didn't find one that numerically included all the points // instead return the closest return(incumbentMiss); } else { return(incumbent); } } }