/// <summary> /// Gets the silhouette of a solid along a given normal. /// </summary> /// <param name="faces"></param> /// <param name="normal"></param> /// <param name="minAngle"></param> /// <param name="minPathAreaToConsider"></param> /// <param name="depthOfPart"></param> /// <returns></returns> public static List <List <PointLight> > Slow(IList <PolygonalFace> faces, double[] normal, double minAngle = 0.1, double minPathAreaToConsider = 0.0, double depthOfPart = 0.0) { var angleTolerance = Math.Cos((90 - minAngle) * Math.PI / 180); //Get the positive faces (defined as face normal along same direction as the silhoutte normal). var positiveFaces = new HashSet <PolygonalFace>(); var vertices = new HashSet <Vertex>(); foreach (var face in faces) { var dot = normal.dotProduct(face.Normal, 3); if (dot.IsGreaterThanNonNegligible(angleTolerance)) { positiveFaces.Add(face); //face.Color = new Color(KnownColors.Blue); foreach (var vertex in face.Vertices) { vertices.Add(vertex); } } } //Project all the vertices into points //The vertex is saved as a reference in the point var transform = MiscFunctions.TransformToXYPlane(normal, out _); var projectedPoints = new Dictionary <int, PointLight>(); foreach (var vertex in vertices) { projectedPoints.Add(vertex.IndexInList, MiscFunctions.Get2DProjectionPointAsLight(vertex, transform)); } //Build a dictionary of faces to polygons //var projectedFacePolygons = positiveFaces.ToDictionary(f => f, f => GetPolygonFromFace(f, projectedPoints, true)); //Use GetPolygonFromFace and force to be positive faces with true" var projectedFacePolygons2 = positiveFaces.Select(f => GetPolygonFromFace(f, projectedPoints, true)).ToList().Where(p => p.Area > minPathAreaToConsider).ToList(); var solution = PolygonOperations.Union(projectedFacePolygons2, false).Select(p => p.Path).ToList(); //Offset by enough to account for minimum angle var scale = Math.Tan(minAngle * Math.PI / 180) * depthOfPart; //Remove tiny polygons and slivers solution = PolygonOperations.SimplifyFuzzy(solution); var offsetPolygons = PolygonOperations.OffsetMiter(solution, scale); var significantSolution = PolygonOperations.OffsetMiter(offsetPolygons, -scale); //Presenter.ShowAndHang(significantSolution); return(significantSolution); //.Select(p => p.Path).ToList(); }
/// <summary> /// Gets the silhouette of a solid along a given normal. Depth of part is only used if removing tiny polygons. /// </summary> /// <param name="faces"></param> /// <param name="normal"></param> /// <param name="originalSolid"></param> /// <param name="minAngle"></param> /// <param name="minPathAreaToConsider"></param> /// <param name="depthOfPart"></param> /// <returns></returns> public static List <List <PointLight> > Run(IList <PolygonalFace> faces, double[] normal, TessellatedSolid originalSolid, double minAngle = 0.1, double minPathAreaToConsider = 0.0, double depthOfPart = 0.0) { //Get the positive faces into a dictionary if (minAngle > 4.999) { minAngle = 4.999; //min angle must be between 0 and 5 degrees. 0.1 degree has proven to be good. } //Note also that the offset is based on the min angle. var angleTolerance = Math.Cos((90 - minAngle) * Math.PI / 180); //Angle of 89.9 Degrees from normal var angleTolerance2 = Math.Cos((90 - 5) * Math.PI / 180); //Angle of 85 Degrees from normal var positiveFaces = new HashSet <PolygonalFace>(); var smallFaces = new List <PolygonalFace>(); var allPositives = new Dictionary <int, PolygonalFace>(); var allVertices = new HashSet <Vertex>(); var positiveEdgeFaces = new HashSet <PolygonalFace>(); foreach (var face in faces) { if (face.Area.IsNegligible()) { continue; } var dot = normal.dotProduct(face.Normal, 3); if (dot.IsGreaterThanNonNegligible(angleTolerance2)) { allPositives.Add(face.IndexInList, face); positiveFaces.Add(face); } else if (dot.IsGreaterThanNonNegligible(angleTolerance)) { //allPositives.Add(face.IndexInList, face); positiveEdgeFaces.Add(face); } else if (Math.Sign(dot) > 0 && face.Area < 1.0) { smallFaces.Add(face); } foreach (var vertex in face.Vertices) { allVertices.Add(vertex); } } //Add any small sliver faces that are sandwinched between two positive faces. foreach (var smallFace in smallFaces) { var largerEdges = smallFace.Edges.OrderBy(e => e.Length).Take(2).ToList(); var addToPositives = true; foreach (var edge in largerEdges) { if (edge.OwnedFace == smallFace && allPositives.ContainsKey(edge.OtherFace.IndexInList)) { } else if (edge.OtherFace == smallFace && allPositives.ContainsKey(edge.OwnedFace.IndexInList)) { } else { addToPositives = false; } } if (addToPositives) { //allPositives.Add(smallFace.IndexInList, smallFace); positiveEdgeFaces.Add(smallFace); } } //Get the polygons of all the positive faces. Force the polygons to be positive CCW var vertices = new HashSet <Vertex>(); foreach (var face in allPositives.Values) { foreach (var vertex in face.Vertices) { vertices.Add(vertex); } } var transform = MiscFunctions.TransformToXYPlane(normal, out _); var projectedPoints = new Dictionary <int, PointLight>(); foreach (var vertex in vertices) { projectedPoints.Add(vertex.IndexInList, MiscFunctions.Get2DProjectionPointAsLight(vertex, transform)); } var projectedFacePolygons = positiveFaces.ToDictionary(f => f.IndexInList, f => GetPathFromFace(f, projectedPoints, true)); //Get all the surfaces var allSurfaces = SeperateIntoSurfaces(allPositives); //var colors = new List<Color>() //{ // new Color(KnownColors.Blue), // new Color(KnownColors.Red), // new Color(KnownColors.Green), // new Color(KnownColors.Yellow), // new Color(KnownColors.Purple), // new Color(KnownColors.Pink), // new Color(KnownColors.Orange), // new Color(KnownColors.Turquoise), // new Color(KnownColors.White), // new Color(KnownColors.Tan) //}; //originalSolid.HasUniformColor = false; //var i = 0; //foreach (var surface in allSurfaces) //{ // if (i == colors.Count) i = 0; // var color = colors[i]; // i++; // foreach (var face in surface) // { // face.Color = color; // } //} //Presenter.ShowAndHang(originalSolid); //Get the surface paths from all the surfaces and union them together var solution = GetSurfacePaths(allSurfaces, normal, minPathAreaToConsider, originalSolid, projectedFacePolygons).ToList(); var positiveEdgeFacePolygons = new List <List <PointLight> >(); foreach (var face in positiveEdgeFaces) { var polygon = new PolygonLight(MiscFunctions.Get2DProjectionPointsAsLight(face.Vertices, normal)); if (!polygon.IsPositive) { polygon.Path.Reverse(); } positiveEdgeFacePolygons.Add(polygon.Path); } try //Try to merge them all at once { solution = PolygonOperations.Union(solution, positiveEdgeFacePolygons, false, PolygonFillType.NonZero); } catch { //Do them one at a time, skipping those that fail foreach (var face in positiveEdgeFacePolygons) { try { solution = PolygonOperations.Union(solution, face, false, PolygonFillType.NonZero); } catch { continue; } } } //Offset by enough to account for minimum angle var scale = Math.Tan(minAngle * Math.PI / 180) * depthOfPart; //Remove tiny polygons and slivers //First, Offset out and then perform a quick check for overhang polygons. //This is helpful when the polygon is nearly self-intersecting. //Then offset back out. solution = PolygonOperations.SimplifyFuzzy(solution, Math.Min(scale / 1000, Constants.LineLengthMinimum), Math.Min(angleTolerance / 1000, Constants.LineSlopeTolerance)); var offsetPolygons = PolygonOperations.OffsetMiter(solution, scale); offsetPolygons = EliminateOverhangPolygons(offsetPolygons, projectedFacePolygons); var significantSolution = PolygonOperations.OffsetMiter(offsetPolygons, -scale); return(significantSolution); }
/// <summary> /// Rotating the calipers 2D method. Convex hull must be a counter clockwise loop. /// </summary> /// <param name="points">The points.</param> /// <param name="pointsAreConvexHull">if set to <c>true</c> [points are convex hull].</param> /// <returns>System.Double.</returns> /// <exception cref="Exception"> /// Area should never be negligilbe unless data is messed up. /// </exception> private static BoundingRectangle RotatingCalipers2DMethod(IList <PointLight> points, bool pointsAreConvexHull = false) { if (points.Count < 3) { throw new Exception("Rotating Calipers requires at least 3 points."); } var cvxPoints = pointsAreConvexHull ? points : ConvexHull2D(points).ToList(); //Simplify the points to make sure they are the minimal convex hull //Only set it as the convex hull if it contains more than three points. var cvxPointsSimple = PolygonOperations.SimplifyFuzzy(cvxPoints); if (cvxPointsSimple.Count >= 3) { cvxPoints = cvxPointsSimple; } /* the cvxPoints will be arranged from a point with minimum X-value around in a CCW loop to the last point */ //First, check to make sure the given convex hull has the min x-value at 0. var minX = cvxPoints[0].X; var numCvxPoints = cvxPoints.Count; var startIndex = 0; for (var i = 1; i < numCvxPoints; i++) { if (!(cvxPoints[i].X < minX)) { continue; } minX = cvxPoints[i].X; startIndex = i; } //Reorder if necessary var tempList = new List <PointLight>(); if (startIndex != 0) { for (var i = startIndex; i < numCvxPoints; i++) { tempList.Add(cvxPoints[i]); } for (var i = 0; i < startIndex; i++) { tempList.Add(cvxPoints[i]); } cvxPoints = tempList; } var extremeIndices = new int[4]; //Good picture of extreme vertices in the following link //http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.155.5671&rep=rep1&type=pdf //Godfried Toussaint: Solving Geometric Problems with the Rotating Calipers //Note that while these points are ordered counter clockwise, we are rotating the calipers in reverse (clockwise), //Which is why the points are directed this way. //Point0 = min X, with max Y for ties //Point1 = min Y, with min X for ties //Point2 = max X, with min Y for ties //Point3 = max Y, with max X for ties // extremeIndices[3] => max-Y, with max X for ties extremeIndices[3] = cvxPoints.Count - 1; // this is likely rare, but first we check if the first point has a higher y value (only when point is both min-x and max-Y) if (cvxPoints[0].Y > cvxPoints[extremeIndices[3]].Y) { extremeIndices[3] = 0; } else { while (extremeIndices[3] > 0 && cvxPoints[extremeIndices[3]].Y <= cvxPoints[extremeIndices[3] - 1].Y) { extremeIndices[3]--; } } /* at this point, the max-Y point has been established. Next we walk backwards in the list until we hit the max-X point */ // extremeIndices[2] => max-X, with min Y for ties extremeIndices[2] = extremeIndices[3] == 0 ? cvxPoints.Count - 1 : extremeIndices[3]; while (extremeIndices[2] > 0 && cvxPoints[extremeIndices[2]].X <= cvxPoints[extremeIndices[2] - 1].X) { extremeIndices[2]--; } // extremeIndices[1] => min-Y, with min X for ties extremeIndices[1] = extremeIndices[2] == 0 ? cvxPoints.Count - 1 : extremeIndices[2]; while (extremeIndices[1] > 0 && cvxPoints[extremeIndices[1]].Y >= cvxPoints[extremeIndices[1] - 1].Y) { extremeIndices[1]--; } // extrememIndices[0] => min-X, with max Y for ties // First we check if the last point has an eqaully small x value, if it does we will need to walk backwards. if (cvxPoints.Last().X > cvxPoints[0].X) { extremeIndices[0] = 0; } else { extremeIndices[0] = cvxPoints.Count - 1; while (cvxPoints[extremeIndices[0]].X >= cvxPoints[extremeIndices[0] - 1].X) { extremeIndices[0]--; } } #region Cycle through 90-degrees var deltaAngles = new double[4]; var offsetAngles = new[] { Math.PI / 2, Math.PI, -Math.PI / 2, 0.0 }; var bestRectangle = new BoundingRectangle { Area = double.PositiveInfinity }; var bestRectanglePointsOnSide = new List <PointLight> [4]; do { #region update the deltaAngles from the current orientation //For each of the 4 supporting points (those forming the rectangle), for (var i = 0; i < 4; i++) { var index = extremeIndices[i]; var prev = index == 0 ? numCvxPoints - 1 : index - 1; var tempDelta = Math.Atan2(cvxPoints[prev].Y - cvxPoints[index].Y, cvxPoints[prev].X - cvxPoints[index].X); deltaAngles[i] = offsetAngles[i] - tempDelta; //If the angle has rotated beyond the 90 degree bounds, it will be negative //And should never be chosen from then on. if (deltaAngles[i] < 0) { deltaAngles[i] = double.PositiveInfinity; } } var angle = deltaAngles.Min(); if (angle.IsGreaterThanNonNegligible(Math.PI / 2)) { break; } var refIndex = deltaAngles.FindIndex(angle); #endregion #region find area //Get unit normal for current edge var otherIndex = extremeIndices[refIndex] == 0 ? numCvxPoints - 1 : extremeIndices[refIndex] - 1; var direction = cvxPoints[extremeIndices[refIndex]].Position.subtract(cvxPoints[otherIndex].Position) .normalize(); //If point type = 1 or 3, then use inversed Direction if (refIndex == 1 || refIndex == 3) { direction = new[] { -direction[1], direction[0] }; } var vectorWidth = new[] { cvxPoints[extremeIndices[2]].X - cvxPoints[extremeIndices[0]].X, cvxPoints[extremeIndices[2]].Y - cvxPoints[extremeIndices[0]].Y }; var angleVector1 = new[] { -direction[1], direction[0] }; var width = Math.Abs(vectorWidth.dotProduct(angleVector1)); var vectorHeight = new[] { cvxPoints[extremeIndices[3]].X - cvxPoints[extremeIndices[1]].X, cvxPoints[extremeIndices[3]].Y - cvxPoints[extremeIndices[1]].Y }; var angleVector2 = new[] { direction[0], direction[1] }; var height = Math.Abs(vectorHeight.dotProduct(angleVector2)); var area = height * width; #endregion var xDir = new[] { angleVector1[0], angleVector1[1] }; var yDir = new[] { angleVector2[0], angleVector2[1] }; var pointsOnSides = new List <PointLight> [4]; for (var i = 0; i < 4; i++) { pointsOnSides[i] = new List <PointLight>(); var dir = i % 2 == 0 ? xDir : yDir; var distance = cvxPoints[extremeIndices[i]].Position.dotProduct(dir, 2); var prevIndex = extremeIndices[i]; do { extremeIndices[i] = prevIndex; pointsOnSides[i].Add(cvxPoints[extremeIndices[i]]); prevIndex = extremeIndices[i] == 0 ? numCvxPoints - 1 : extremeIndices[i] - 1; } while (distance.IsPracticallySame(cvxPoints[prevIndex].Position.dotProduct(dir, 2), Constants.BaseTolerance)); } if (bestRectangle.Area > area) { bestRectanglePointsOnSide = pointsOnSides; bestRectangle.Area = area; bestRectangle.Dimensions = new[] { width, height }; bestRectangle.Directions2D = new[] { xDir, yDir }; } } while (true); //process will end on its own by the break statement in line 314 #endregion var pointsOnSidesAsPoints = new List <Point> [4]; for (var i = 0; i < 4; i++) { pointsOnSidesAsPoints[i] = bestRectanglePointsOnSide[i].Select(p => new Point(p)).ToList(); } bestRectangle.PointsOnSides = pointsOnSidesAsPoints; if (bestRectangle.Area.IsNegligible()) { throw new Exception("Area should never be negligilbe unless data is messed up."); } bestRectangle.SetCornerPoints(); return(bestRectangle); }
/// <summary> /// Rotating the calipers 2D method. Convex hull must be a counter clockwise loop. /// Optional booleans for what information should be set in the Bounding Rectangle. /// Example: If you really just need the area, you don't need the corner points or /// points on side. /// </summary> /// <param name="points">The points.</param> /// <param name="pointsAreConvexHull">if set to <c>true</c> [points are convex hull].</param> /// <param name="setCornerPoints"></param> /// <param name="setPointsOnSide"></param> /// <returns>System.Double.</returns> /// <exception cref="Exception"> /// Area should never be negligilbe unless data is messed up. /// </exception> /* * private static BoundingRectangle RotatingCalipers2DMethod(IList<Point> points, bool pointsAreConvexHull = false, * bool setCornerPoints = true, bool setPointsOnSide = true) * { #region Initialization * if(points.Count < 3) throw new Exception("Rotating Calipers requires at least 3 points."); * var cvxPoints = pointsAreConvexHull ? points : ConvexHull2D(points); * //Simplify the points to make sure they are the minimal convex hull * //Only set it as the convex hull if it contains more than three points. * var cvxPointsSimple = PolygonOperations.SimplifyFuzzy(cvxPoints); * if (cvxPointsSimple.Count >= 3) cvxPoints = cvxPointsSimple; * // the cvxPoints will be arranged from a point with minimum X-value around in a CCW loop to the last point * //First, check to make sure the given convex hull has the min x-value at 0. * var minX = cvxPoints[0].X; * var numCvxPoints = cvxPoints.Count; * var startIndex = 0; * for (var i = 1; i < numCvxPoints; i++) * { * if (!(cvxPoints[i].X < minX)) continue; * minX = cvxPoints[i].X; * startIndex = i; * } * //Reorder if necessary * var tempList = new List<Point>(); * if (startIndex != 0) * { * for (var i = startIndex; i < numCvxPoints; i++) * { * tempList.Add(cvxPoints[i]); * } * for (var i = 0; i < startIndex; i++) * { * tempList.Add(cvxPoints[i]); * } * cvxPoints = tempList; * } * * * var extremeIndices = new int[4]; * * //Good picture of extreme vertices in the following link * //http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.155.5671&rep=rep1&type=pdf * //Godfried Toussaint: Solving Geometric Problems with the Rotating Calipers * //Note that while these points are ordered counter clockwise, we are rotating the calipers in reverse (clockwise), * //Which is why the points are directed this way. * //Point0 = min X, with max Y for ties * //Point1 = min Y, with min X for ties * //Point2 = max X, with min Y for ties * //Point3 = max Y, with max X for ties * * // extremeIndices[3] => max-Y, with max X for ties * extremeIndices[3] = cvxPoints.Count - 1; * // this is likely rare, but first we check if the first point has a higher y value (only when point is both min-x and max-Y) * if (cvxPoints[0].Y > cvxPoints[extremeIndices[3]].Y) extremeIndices[3] = 0; * else * { * while (extremeIndices[3] > 0 && cvxPoints[extremeIndices[3]].Y <= cvxPoints[extremeIndices[3] - 1].Y) * extremeIndices[3]--; * } * // at this point, the max-Y point has been established. Next we walk backwards in the list until we hit the max-X point * // extremeIndices[2] => max-X, with min Y for ties * extremeIndices[2] = extremeIndices[3] == 0 ? cvxPoints.Count - 1 : extremeIndices[3]; * while (extremeIndices[2] > 0 && cvxPoints[extremeIndices[2]].X <= cvxPoints[extremeIndices[2] - 1].X) * extremeIndices[2]--; * // extremeIndices[1] => min-Y, with min X for ties * extremeIndices[1] = extremeIndices[2] == 0 ? cvxPoints.Count - 1 : extremeIndices[2]; * while (extremeIndices[1] > 0 && cvxPoints[extremeIndices[1]].Y >= cvxPoints[extremeIndices[1] - 1].Y) * extremeIndices[1]--; * // extrememIndices[0] => min-X, with max Y for ties * // First we check if the last point has an eqaully small x value, if it does we will need to walk backwards. * if (cvxPoints.Last().X > cvxPoints[0].X) extremeIndices[0] = 0; * else * { * extremeIndices[0] = cvxPoints.Count - 1; * while (cvxPoints[extremeIndices[0]].X >= cvxPoints[extremeIndices[0] - 1].X) * extremeIndices[0]--; * } * #endregion * #region Cycle through 90-degrees * var bestRectangle = new BoundingRectangle { Area = double.MaxValue }; * var deltaAngles = new double[4]; * do * { #region update the deltaAngles from the current orientation * * //For each of the 4 supporting points (those forming the rectangle), * for (var i = 0; i < 4; i++) * { * var index = extremeIndices[i]; * var prev = index == 0 ? numCvxPoints - 1 : index - 1; * var tempDelta = Math.Atan2(cvxPoints[prev].Y - cvxPoints[index].Y, * cvxPoints[prev].X - cvxPoints[index].X); * deltaAngles[i] = CaliperOffsetAngles[i] - tempDelta; * //If the angle has rotated beyond the 90 degree bounds, it will be negative * //And should never be chosen from then on. * if (deltaAngles[i] < 0) deltaAngles[i] = double.PositiveInfinity; * } * var angle = deltaAngles.Min(); * if (angle.IsGreaterThanNonNegligible(Math.PI/2)) * break; * var refIndex = deltaAngles.FindIndex(angle); * #endregion * #region find area * * //Get unit normal for current edge * var otherIndex = extremeIndices[refIndex] == 0 ? numCvxPoints - 1 : extremeIndices[refIndex] - 1; * var direction = * cvxPoints[extremeIndices[refIndex]].Position.subtract(cvxPoints[otherIndex].Position, 2) * .normalize(2); * //If point type = 1 or 3, then use inversed Direction * if (refIndex == 1 || refIndex == 3) * { * direction = new[] {-direction[1], direction[0]}; * } * var vectorLength = new[] * { * cvxPoints[extremeIndices[2]][0] - cvxPoints[extremeIndices[0]][0], * cvxPoints[extremeIndices[2]][1] - cvxPoints[extremeIndices[0]][1] * }; * * var angleVector1 = new[] {-direction[1], direction[0]}; * var length = Math.Abs(vectorLength.dotProduct(angleVector1, 2)); * var vectorWidth = new[] * { * cvxPoints[extremeIndices[3]][0] - cvxPoints[extremeIndices[1]][0], * cvxPoints[extremeIndices[3]][1] - cvxPoints[extremeIndices[1]][1] * }; * var angleVector2 = new[] {direction[0], direction[1]}; * var width = Math.Abs(vectorWidth.dotProduct(angleVector2, 2)); * var area = length * width; * #endregion * * var d1Max = double.MinValue; * var d1Min = double.MaxValue; * var d2Max = double.MinValue; * var d2Min = double.MaxValue; * var pointsOnSides = new List<Point>[4]; * for (var i = 0; i < 4; i++) * { * pointsOnSides[i] = new List<Point>(); * var dir = i%2 == 0 ? angleVector1 : angleVector2; * var distance = cvxPoints[extremeIndices[i]].Position.dotProduct(dir, 2); * if (i % 2 == 0) //D1 * { * if (distance > d1Max) d1Max = distance; * if (distance < d1Min) d1Min = distance; * } * else //D2 * { * if (distance > d2Max) d2Max = distance; * if (distance < d2Min) d2Min = distance; * } * var prevIndex = extremeIndices[i]; * do * { * extremeIndices[i] = prevIndex; * if (setPointsOnSide) * { * pointsOnSides[i].Add(cvxPoints[extremeIndices[i]]); * } * prevIndex = extremeIndices[i] == 0 ? numCvxPoints - 1 : extremeIndices[i] - 1; * } while (distance.IsPracticallySame(cvxPoints[prevIndex].Position.dotProduct(dir, 2), * Constants.BaseTolerance)); * } * * //If this is an improvement, set the parameters for the best bounding rectangle. * if (area < bestRectangle.Area) * { * bestRectangle.Area = area; * bestRectangle.Length = length; //Length corresponds with Direction 1 * bestRectangle.Width = width; * bestRectangle.LengthDirection = angleVector1; * bestRectangle.WidthDirection = angleVector2; * bestRectangle.LengthDirectionMax = d1Max; * bestRectangle.LengthDirectionMin = d1Min; * bestRectangle.WidthDirectionMax = d2Max; * bestRectangle.WidthDirectionMin = d2Min; * bestRectangle.PointsOnSides = pointsOnSides; * } * } while (true); //process will end on its own by the break statement in line 314 * #endregion * * if (bestRectangle.Area.IsNegligible()) * throw new Exception("Area should never be negligilbe unless data is messed up."); * * if (setCornerPoints) * bestRectangle.SetCornerPoints(); * * return bestRectangle; * } */ /// <summary> /// Rotating the calipers 2D method. Convex hull must be a counter clockwise loop. /// Optional booleans for what information should be set in the Bounding Rectangle. /// Example: If you really just need the area, you don't need the corner points or /// points on side. /// </summary> /// <param name="points">The points.</param> /// <param name="pointsAreConvexHull">if set to <c>true</c> [points are convex hull].</param> /// <param name="setCornerPoints"></param> /// <param name="setPointsOnSide"></param> /// <returns>System.Double.</returns> /// <exception cref="Exception"> /// Area should never be negligible unless data is messed up. /// </exception> private static BoundingRectangle RotatingCalipers2DMethod(IList <PointLight> points, bool pointsAreConvexHull = false, bool setCornerPoints = true, bool setPointsOnSide = true) { if (points.Count < 3) { throw new Exception("Rotating Calipers requires at least 3 points."); } var cvxPoints = pointsAreConvexHull ? points : ConvexHull2D(points).ToList(); //Simplify the points to make sure they are the minimal convex hull //Only set it as the convex hull if it contains more than three points. var cvxPointsSimple = PolygonOperations.SimplifyFuzzy(cvxPoints); if (cvxPointsSimple.Count >= 3) { cvxPoints = cvxPointsSimple; } /* the cvxPoints will be arranged from a point with minimum X-value around in a CCW loop to the last point */ //First, check to make sure the given convex hull has the min x-value at 0. var minX = cvxPoints[0].X; var numCvxPoints = cvxPoints.Count; var startIndex = 0; for (var i = 1; i < numCvxPoints; i++) { if (!(cvxPoints[i].X < minX)) { continue; } minX = cvxPoints[i].X; startIndex = i; } //Reorder if necessary var tempList = new List <PointLight>(); if (startIndex != 0) { for (var i = startIndex; i < numCvxPoints; i++) { tempList.Add(cvxPoints[i]); } for (var i = 0; i < startIndex; i++) { tempList.Add(cvxPoints[i]); } cvxPoints = tempList; } #region Get Extreme Points var extremeIndices = new int[4]; //Good picture of extreme vertices in the following link //http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.155.5671&rep=rep1&type=pdf //Godfried Toussaint: Solving Geometric Problems with the Rotating Calipers //Note that while these points are ordered counter clockwise, we are rotating the calipers in reverse (clockwise), //Which is why the points are directed this way. //Point0 = min X, with max Y for ties //Point1 = min Y, with min X for ties //Point2 = max X, with min Y for ties //Point3 = max Y, with max X for ties // extremeIndices[3] => max-Y, with max X for ties extremeIndices[3] = cvxPoints.Count - 1; // this is likely rare, but first we check if the first point has a higher y value (only when point is both min-x and max-Y) if (cvxPoints[0].Y > cvxPoints[extremeIndices[3]].Y) { extremeIndices[3] = 0; } else { while (extremeIndices[3] > 0 && cvxPoints[extremeIndices[3]].Y <= cvxPoints[extremeIndices[3] - 1].Y) { extremeIndices[3]--; } } /* at this point, the max-Y point has been established. Next we walk backwards in the list until we hit the max-X point */ // extremeIndices[2] => max-X, with min Y for ties extremeIndices[2] = extremeIndices[3] == 0 ? cvxPoints.Count - 1 : extremeIndices[3]; while (extremeIndices[2] > 0 && cvxPoints[extremeIndices[2]].X <= cvxPoints[extremeIndices[2] - 1].X) { extremeIndices[2]--; } // extremeIndices[1] => min-Y, with min X for ties extremeIndices[1] = extremeIndices[2] == 0 ? cvxPoints.Count - 1 : extremeIndices[2]; while (extremeIndices[1] > 0 && cvxPoints[extremeIndices[1]].Y >= cvxPoints[extremeIndices[1] - 1].Y) { extremeIndices[1]--; } // extrememIndices[0] => min-X, with max Y for ties // First we check if the last point has an eqaully small x value, if it does we will need to walk backwards. if (cvxPoints.Last().X > cvxPoints[0].X) { extremeIndices[0] = 0; } else { extremeIndices[0] = cvxPoints.Count - 1; while (cvxPoints[extremeIndices[0]].X >= cvxPoints[extremeIndices[0] - 1].X) { extremeIndices[0]--; } } #endregion var bestRectangle = new BoundingRectangle { Area = double.MaxValue }; var PointsOnSides = new List <PointLight> [4]; #region Cycle through 90-degrees var deltaAngles = new double[4]; do { #region update the deltaAngles from the current orientation //For each of the 4 supporting points (those forming the rectangle), var minAngle = double.PositiveInfinity; var refIndex = 0; for (var i = 0; i < 4; i++) { var index = extremeIndices[i]; var prev = index == 0 ? cvxPoints[numCvxPoints - 1] : cvxPoints[index - 1]; var current = cvxPoints[index]; var tempDelta = Math.Atan2(prev.Y - current.Y, prev.X - current.X); var angle = CaliperOffsetAngles[i] - tempDelta; //If the angle has rotated beyond the 90 degree bounds, it will be negative //And should never be chosen from then on. if (angle < 0) { deltaAngles[i] = double.PositiveInfinity; } else { deltaAngles[i] = angle; if (angle < minAngle) { minAngle = angle; refIndex = i; } } } if (minAngle.IsGreaterThanNonNegligible(Math.PI / 2)) { break; } #endregion #region find area //Get unit normal for current edge var otherIndex = extremeIndices[refIndex] == 0 ? numCvxPoints - 1 : extremeIndices[refIndex] - 1; var direction = cvxPoints[extremeIndices[refIndex]].Subtract(cvxPoints[otherIndex]).normalize(2); //If point type = 1 or 3, then use inversed Direction if (refIndex == 1 || refIndex == 3) { direction = new[] { -direction[1], direction[0] }; } var vectorLength = new[] { cvxPoints[extremeIndices[2]].X - cvxPoints[extremeIndices[0]].X, cvxPoints[extremeIndices[2]].Y - cvxPoints[extremeIndices[0]].Y }; var angleVector1 = new[] { -direction[1], direction[0] }; var length = Math.Abs(vectorLength.dotProduct(angleVector1, 2)); var vectorWidth = new[] { cvxPoints[extremeIndices[3]].X - cvxPoints[extremeIndices[1]].X, cvxPoints[extremeIndices[3]].Y - cvxPoints[extremeIndices[1]].Y }; var angleVector2 = new[] { direction[0], direction[1] }; var width = Math.Abs(vectorWidth.dotProduct(angleVector2, 2)); var area = length * width; #endregion var d1Max = double.MinValue; var d1Min = double.MaxValue; var d2Max = double.MinValue; var d2Min = double.MaxValue; var pointsOnSides = new List <PointLight> [4]; for (var i = 0; i < 4; i++) { pointsOnSides[i] = new List <PointLight>(); var dir = i % 2 == 0 ? angleVector1 : angleVector2; var distance = cvxPoints[extremeIndices[i]].dotProduct(dir); if (i % 2 == 0) //D1 { if (distance > d1Max) { d1Max = distance; } if (distance < d1Min) { d1Min = distance; } } else //D2 { if (distance > d2Max) { d2Max = distance; } if (distance < d2Min) { d2Min = distance; } } var prevIndex = extremeIndices[i]; do { extremeIndices[i] = prevIndex; if (setPointsOnSide) { pointsOnSides[i].Add(cvxPoints[extremeIndices[i]]); } prevIndex = extremeIndices[i] == 0 ? numCvxPoints - 1 : extremeIndices[i] - 1; } while (distance.IsPracticallySame(cvxPoints[prevIndex].dotProduct(dir), Constants.BaseTolerance)); } //If this is an improvement, set the parameters for the best bounding rectangle. if (area < bestRectangle.Area) { bestRectangle.Area = area; bestRectangle.Length = length; //Lenght corresponds with direction 1. bestRectangle.Width = width; bestRectangle.LengthDirection = angleVector1; bestRectangle.WidthDirection = angleVector2; bestRectangle.LengthDirectionMax = d1Max; bestRectangle.LengthDirectionMin = d1Min; bestRectangle.WidthDirectionMax = d2Max; bestRectangle.WidthDirectionMin = d2Min; PointsOnSides = pointsOnSides; } } while (true); //process will end on its own by the break statement in line 314 #endregion if (bestRectangle.Area.IsNegligible()) { var polygon = new Polygon(cvxPoints.Select(p => new Point(p))); var allPoints = new List <PointLight>(points); if (!polygon.IsConvex()) { var c = 0; var random = new Random(1); //Use a specific random generator to make this repeatable var pointCount = allPoints.Count; while (pointCount > 10 && c < 10) //Ten points would be ideal { //Remove a random point var max = pointCount - 1; var index = random.Next(0, max); var point = allPoints[index]; allPoints.RemoveAt(index); //Check if it is still invalid var newConvexHull = ConvexHull2D(allPoints).ToList(); polygon = new Polygon(newConvexHull.Select(p => new Point(p))); if (polygon.IsConvex()) { //Don't remove the point c++; allPoints.Insert(index, point); } else { pointCount--; } } } var polyLight = new PolygonLight(allPoints); var date = DateTime.Now.ToString("MM.dd.yy_HH.mm"); polyLight.Serialize("ConvexHullError_" + date + ".PolyLight"); var cvxHullLight = new PolygonLight(polygon); cvxHullLight.Serialize("ConvexHull_" + date + ".PolyLight"); throw new Exception("Error in Minimum Bounding Box, likely due to faulty convex hull."); } if (setCornerPoints) { bestRectangle.SetCornerPoints(); } if (setPointsOnSide) { var pointsOnSidesAsPoints = new List <Point> [4]; for (var i = 0; i < 4; i++) { pointsOnSidesAsPoints[i] = PointsOnSides[i].Select(p => new Point(p)).ToList(); } bestRectangle.PointsOnSides = pointsOnSidesAsPoints; } return(bestRectangle); }