public static CrossSectionSolid CreateConstantCrossSectionSolid(double[] buildDirection, double extrudeDistance, List <Vertex> layer3DAtStart, double sameTolerance, UnitType units) { //Since the start point may be along a negative direction, we have to add vectors instead of adding the extrudeDistance as is. var start = layer3DAtStart.First().Position.dotProduct(buildDirection); var endPoint = layer3DAtStart.First().Position.add(buildDirection.multiply(extrudeDistance)); var stepDistances = new Dictionary <int, double> { { 0, start }, { 1, endPoint.dotProduct(buildDirection) } }; var shape = new PolygonLight(MiscFunctions.Get2DProjectionPointsAsLight(layer3DAtStart, buildDirection)); if (shape.Area < 0) { shape = PolygonLight.Reverse(shape); } var layers2D = new Dictionary <int, List <PolygonLight> > { { 0, new List <PolygonLight> { shape } }, { 1, new List <PolygonLight> { shape } } }; return(new CrossSectionSolid(buildDirection, stepDistances, sameTolerance, layers2D, null, units)); }
public static PolygonLight Reverse(PolygonLight original) { var path = new List <PointLight>(original.Path); path.Reverse(); var newPoly = new PolygonLight(path); return(newPoly); }
public void Add(List <Vertex> feature3D, PolygonLight feature2D, int layer) { if (!Layer3D.ContainsKey(layer)) { Layer3D[layer] = new List <List <Vertex> >(); Layer2D[layer] = new List <PolygonLight>(); } //Layer 3D is optional and may be null, but Layer2D cannot be null. if (feature3D != null) { Layer3D[layer].Add(feature3D); } Layer2D[layer].Add(feature2D); }
private static PolygonLight GetPolygonFromFace(PolygonalFace face, Dictionary <int, PointLight> projectedPoints, bool forceToBePositive) { if (face.Vertices.Count != 3) { throw new Exception("This method was only developed with triangles in mind."); } //Make sure the polygon is ordered correctly (we already know this face is positive) var points = face.Vertices.Select(v => projectedPoints[v.IndexInList]).ToList(); var facePolygon = new PolygonLight(points); if (forceToBePositive && facePolygon.Area < 0) { facePolygon = PolygonLight.Reverse(facePolygon); } return(facePolygon); }
protected void OnDeserializedMethod(StreamingContext context) { JArray jArray = (JArray)serializationData["CrossSections"]; var layerArray = jArray.ToObject <double[][][]>(); Layer2D = new Dictionary <int, List <PolygonLight> >(); var j = 0; for (int i = FirstIndex; i <= LastIndex; i++) { var layer = new List <PolygonLight>(); foreach (var coordinates in layerArray[j]) { layer.Add(PolygonLight.MakeFromBinaryString(coordinates)); } Layer2D.Add(i, layer); j++; } }
protected void OnDeserializedMethod(StreamingContext context) { JArray jArray = (JArray)serializationData["CrossSections"]; var layerArray = jArray.ToObject <double[][][]>(); Layer2D = new Dictionary <int, List <PolygonLight> >(); var keysArray = StepDistances.Keys.ToArray(); for (int i = 0; i < layerArray.Length; i++) { var layer = new List <PolygonLight>(); var key = keysArray[i]; foreach (var coordinates in layerArray[i]) { layer.Add(PolygonLight.MakeFromBinaryString(coordinates)); } Layer2D.Add(key, layer); } }
public void SetCrossSection2D(Flat plane) { var paths = new List <List <PointLight> >(); var positivePath = new PolygonLight(MiscFunctions.Get2DProjectionPointsAsLight(PositiveLoop.VertexLoop, plane.Normal).ToList()); if (positivePath.Area < 0) { positivePath.Path.Reverse(); } paths.Add(positivePath.Path); foreach (var loop in NegativeLoops) { var negativePath = new PolygonLight(MiscFunctions.Get2DProjectionPointsAsLight(loop.VertexLoop, plane.Normal).ToList()); if (negativePath.Area > 0) { negativePath.Path.Reverse(); } paths.Add(negativePath.Path); } CrossSection2D = PolygonOperations.Union(paths).Select(p => new PolygonLight(p)).ToList(); }
public Polygon(PolygonLight poly) : this(poly.Path.Select(p => new Point(p))) { }
/// <summary> /// Create the Polygonal Faces for a new Tesselated Solid by extruding the given loop along the given normal. /// Setting midPlane to true, extrudes half forward and half reverse. /// </summary> /// <param name="loops"></param> /// <param name="extrudeDirection"></param> /// <param name="distance"></param> /// <param name="midPlane"></param> /// <returns></returns> public static List <PolygonalFace> ReturnFacesFromLoops(IEnumerable <IEnumerable <double[]> > loops, double[] extrudeDirection, double distance, bool midPlane = false) { //This simplifies the cases we have to handle by always extruding in the positive direction if (distance < 0) { distance = -distance; extrudeDirection = extrudeDirection.multiply(-1); } //First, make sure we are using "clean" loops. (e.g. not connected to any faces or edges) var cleanLoops = new List <List <Vertex> >(); var i = 0; foreach (var loop in loops) { var cleanLoop = new List <Vertex>(); foreach (var vertexPosition in loop) { //If a midPlane extrusion, move the original vertices backwards by 1/2 the extrude distance. //These vertices will be used as the base for offsetting the paired vertices forward by the //entire extrude distance. if (midPlane) { var midPlaneVertexPosition = vertexPosition.add(extrudeDirection.multiply(-distance / 2), 3); cleanLoop.Add(new Vertex(midPlaneVertexPosition, i)); } else { cleanLoop.Add(new Vertex(vertexPosition, i)); } i++; } cleanLoops.Add(cleanLoop); } var distanceFromOriginAlongDirection = extrudeDirection.dotProduct(cleanLoops.First().First().Position, 3); //First, triangulate the loops var listOfFaces = new List <PolygonalFace>(); var backTransform = new double[, ] { }; var paths = cleanLoops.Select(loop => MiscFunctions.Get2DProjectionPointsAsLightReorderingIfNecessary(loop.ToArray(), extrudeDirection, out backTransform)).ToList(); List <PointLight[]> points2D; List <Vertex[]> triangles; try { //Reset the list of triangles triangles = new List <Vertex[]>(); //Do some polygon functions to clean up issues and try again //This is important because the Get2DProjections may produce invalid paths and because //triangulate will try 3 times before throwing the exception to go to the catch. paths = PolygonOperations.Union(paths, true, PolygonFillType.EvenOdd); //Since triangulate polygon needs the points to have references to their vertices, we need to add vertex references to each point //This also means we need to recreate cleanLoops //Also, give the vertices indices. cleanLoops = new List <List <Vertex> >(); points2D = new List <PointLight[]>(); var j = 0; foreach (var path in paths) { var pathAsPoints = path.Select(p => new PointLight(p.X, p.Y, true)).ToArray(); var area = new PolygonLight(path).Area; points2D.Add(pathAsPoints); var cleanLoop = new List <Vertex>(); foreach (var point in pathAsPoints) { var position = new[] { point.X, point.Y, 0.0, 1.0 }; var vertexPosition1 = backTransform.multiply(position).Take(3).ToArray(); //The point has been located back to its original position. It is not necessarily the correct distance along the cutting plane normal. //So, we must move it to be on the plane //This next line gets a second vertex to use for the point on plane function var vertexPosition2 = vertexPosition1.add(extrudeDirection.multiply(5), 3); var vertex = MiscFunctions.PointOnPlaneFromIntersectingLine(extrudeDirection, distanceFromOriginAlongDirection, new Vertex(vertexPosition1), new Vertex(vertexPosition2)); vertex.IndexInList = j; point.References.Add(vertex); cleanLoop.Add(vertex); j++; } cleanLoops.Add(cleanLoop); } bool[] isPositive = null; var triangleFaceList = TriangulatePolygon.Run2D(points2D, out _, ref isPositive); foreach (var face in triangleFaceList) { triangles.AddRange(face); } } catch { try { //Reset the list of triangles triangles = new List <Vertex[]>(); //Do some polygon functions to clean up issues and try again paths = PolygonOperations.Union(paths, true, PolygonFillType.EvenOdd); paths = PolygonOperations.OffsetRound(paths, distance / 1000); paths = PolygonOperations.OffsetRound(paths, -distance / 1000); paths = PolygonOperations.Union(paths, true, PolygonFillType.EvenOdd); //Since triangulate polygon needs the points to have references to their vertices, we need to add vertex references to each point //This also means we need to recreate cleanLoops //Also, give the vertices indices. cleanLoops = new List <List <Vertex> >(); points2D = new List <PointLight[]>(); var j = 0; foreach (var path in paths) { var pathAsPoints = path.Select(p => new PointLight(p.X, p.Y, true)).ToArray(); points2D.Add(pathAsPoints); var cleanLoop = new List <Vertex>(); foreach (var point in pathAsPoints) { var position = new[] { point.X, point.Y, 0.0, 1.0 }; var vertexPosition1 = backTransform.multiply(position).Take(3).ToArray(); //The point has been located back to its original position. It is not necessarily the correct distance along the cutting plane normal. //So, we must move it to be on the plane //This next line gets a second vertex to use for the point on plane function var vertexPosition2 = vertexPosition1.add(extrudeDirection.multiply(5), 3); var vertex = MiscFunctions.PointOnPlaneFromIntersectingLine(extrudeDirection, distanceFromOriginAlongDirection, new Vertex(vertexPosition1), new Vertex(vertexPosition2)); vertex.IndexInList = j; point.References.Add(vertex); cleanLoop.Add(vertex); j++; } cleanLoops.Add(cleanLoop); } bool[] isPositive = null; var triangleFaceList = TriangulatePolygon.Run2D(points2D, out _, ref isPositive); foreach (var face in triangleFaceList) { triangles.AddRange(face); } } catch { Debug.WriteLine("Tried extrusion twice and failed."); return(null); } } //Second, build up the a set of duplicate vertices var vertices = new HashSet <Vertex>(); foreach (var vertex in cleanLoops.SelectMany(loop => loop)) { vertices.Add(vertex); } var pairedVertices = new Dictionary <Vertex, Vertex>(); foreach (var vertex in vertices) { var newVertex = new Vertex(vertex.Position.add(extrudeDirection.multiply(distance), 3)); pairedVertices.Add(vertex, newVertex); } //Third, create the triangles on the two ends //var triangleDictionary = new Dictionary<PolygonalFace, PolygonalFace>(); var topFaces = new List <PolygonalFace>(); foreach (var triangle in triangles) { //Create the triangle in plane with the loops var v1 = triangle[1].Position.subtract(triangle[0].Position, 3); var v2 = triangle[2].Position.subtract(triangle[0].Position, 3); //This model reverses the triangle vertex ordering as necessary to line up with the normal. var topTriangle = v1.crossProduct(v2).dotProduct(extrudeDirection.multiply(-1), 3) < 0 ? new PolygonalFace(triangle.Reverse(), extrudeDirection.multiply(-1), true) : new PolygonalFace(triangle, extrudeDirection.multiply(-1), true); topFaces.Add(topTriangle); listOfFaces.Add(topTriangle); //Create the triangle on the opposite side of the extrusion var bottomTriangle = new PolygonalFace( new List <Vertex> { pairedVertices[triangle[0]], pairedVertices[triangle[2]], pairedVertices[triangle[1]] }, extrudeDirection, false); listOfFaces.Add(bottomTriangle); //triangleDictionary.Add(topTriangle, bottomTriangle); } //Fourth, create the triangles on the sides //The normals of the faces are dependent on the whether the loops are ordered correctly from the view of the extrude direction //This influences which order the vertices are used to create triangles. for (var j = 0; j < cleanLoops.Count; j++) { var loop = cleanLoops[j]; //Determine if the loop direction is correct by using the top face var v1 = loop[0]; var v2 = loop[1]; //Find the face with both of these vertices PolygonalFace firstFace = null; foreach (var face in topFaces) { if (face.Vertices[0] == v1 || face.Vertices[1] == v1 || face.Vertices[2] == v1) { if (face.Vertices[0] == v2 || face.Vertices[1] == v2 || face.Vertices[2] == v2) { firstFace = face; break; } } } if (firstFace == null) { throw new Exception("Did not find face with both the vertices"); } if (firstFace.NextVertexCCW(v1) == v2) { //Do nothing } else if (firstFace.NextVertexCCW(v2) == v1) { //Reverse the loop loop.Reverse(); } else { throw new Exception(); } //The loop is now ordered correctly //It does not matter whether the loop is positive or negative, only that it is ordered correctly for the given extrude direction for (var k = 0; k < loop.Count; k++) { var g = k + 1; if (k == loop.Count - 1) { g = 0; } //Create the new triangles listOfFaces.Add(new PolygonalFace(new List <Vertex>() { loop[k], pairedVertices[loop[k]], pairedVertices[loop[g]] })); listOfFaces.Add(new PolygonalFace(new List <Vertex>() { loop[k], pairedVertices[loop[g]], loop[g] })); } } return(listOfFaces); }
/// <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); }
public Polygon(PolygonLight poly, bool setLines = false) : this(poly.Path.Select(p => new Point(p)), setLines) { }
/// <summary> /// Updates the with. /// </summary> /// <param name="face">The face.</param> public bool BuildIfCylinderIsHole(bool isPositive) { if (isPositive) { throw new Exception("BuildIfCylinderIsHole assumes that the faces have already been collected, " + "such that the cylinder is negative"); } IsPositive = false; //To truly be a hole, there should be two loops of vertices that form circles on either ends of the faces. //These are easy to capture because all the edges between them should be shared by two of the faces //Start by collecting the edges at either end. Each edge belongs to only two faces, so any edge that only //comes up once, must be at the edge of the cylinder (assuming it is a cylinder). var edges = new HashSet <Edge>(); foreach (var face in Faces) { foreach (var edge in face.Edges) { if (edges.Contains(edge)) { edges.Remove(edge); } else { edges.Add(edge); } } } //Now we can loop through the edges to form two loops if (edges.Count < 5) { return(false); //5 is the minimum number of vertices to look remotely circular (8 is more likely) } var(allLoopsClosed, edgeLoops, loops) = GetLoops(edges, true); if (loops.Count != 2) { return(false); //There must be two and only two loops. } Loop1 = new HashSet <Vertex>(loops[0]); Loop2 = new HashSet <Vertex>(loops[1]); EdgeLoop1 = edgeLoops[0]; EdgeLoop2 = edgeLoops[1]; //Next, we need to get the central axis. //The ends of the cylinder could be any shape (flat, curved, angled) and the shapes //on each end do not need to match. This rules out using the vertex loops to form //a plane (most accurate) and creating a plane from edge midpoints (next most accurate). //The next most accurate thing is to use the edge vectors to set the axis. //This is more precise than taking a bunch of cross products with the faces. //And it is more universal than creating a plane from the loops, since it works //for holes that enter and exit at an angle. var throughEdgeVectors = new Dictionary <Vertex, double[]>(); var dotFromSharpestEdgesConnectedToVertex = new Dictionary <Vertex, double>(); foreach (var edge in InnerEdges) { //Skip those edges that are on "flat" surfaces var dot = edge.OwnedFace.Normal.dotProduct(edge.OtherFace.Normal); if (dot.IsPracticallySame(1.0, Constants.ErrorForFaceInSurface)) { continue; } //This uses a for loop to remove duplicate code, to decide which vertex to check with which loop for (var i = 0; i < 2; i++) { var A = i == 0 ? edge.To : edge.From; var B = i == 0 ? edge.From : edge.To; var direction = edge.Vector.normalize(); //Positive if B is further along var previousDistance = direction.dotProduct(B.Position); var sign = Math.Sign(direction.dotProduct(B.Position) - direction.dotProduct(A.Position)); if (Loop1.Contains(A)) { bool reachedEnd = Loop2.Contains(B); if (!reachedEnd) { //Check if this edge needs to "extended" to reach the end of the cylinder var previousEdge = edge; var previousVertex = B; while (!reachedEnd) { var maxDot = 0.0; Edge extensionEdge = null; foreach (var otherEdge in previousVertex.Edges.Where(e => e != previousEdge)) { //This other edge must be contained in the InnerEdges and along the same direction if (!InnerEdges.Contains(otherEdge)) { continue; } var edgeDot = Math.Abs(otherEdge.Vector.normalize().dotProduct(previousEdge.Vector.normalize())); if (!edgeDot.IsPracticallySame(1.0, Constants.ErrorForFaceInSurface)) { continue; } var distance = sign * (direction.dotProduct(otherEdge.OtherVertex(previousVertex).Position) - previousDistance); if (!distance.IsGreaterThanNonNegligible()) { continue; //This vertex is not any further along } //Choose the edge that is most along the previous edge if (edgeDot > maxDot) { maxDot = edgeDot; extensionEdge = otherEdge; } } if (extensionEdge == null) { break; //go to the next edge } if (Loop2.Contains(extensionEdge.OtherVertex(previousVertex))) { reachedEnd = true; B = extensionEdge.OtherVertex(previousVertex); } else { previousVertex = extensionEdge.OtherVertex(previousVertex); previousEdge = extensionEdge; } } } //If there was a vertex from the edge or edges in the second loop. if (reachedEnd) { if (!dotFromSharpestEdgesConnectedToVertex.ContainsKey(A)) { throughEdgeVectors.Add(A, B.Position.subtract(A.Position)); dotFromSharpestEdgesConnectedToVertex.Add(A, edge.InternalAngle); } else if (dot < dotFromSharpestEdgesConnectedToVertex[A]) { throughEdgeVectors[A] = B.Position.subtract(A.Position); dotFromSharpestEdgesConnectedToVertex[A] = dot; } break; //Go to the next edge } } } } if (throughEdgeVectors.Count < 3) { return(false); } //Estimate the axis from the sum of the through edge vectors //The axis points from loop 1 to loop 2, since we always start the edge vector from Loop1 var edgeVectors = new List <double[]>(throughEdgeVectors.Values); var numEdges = edgeVectors.Count; var axis = new double[] { 0.0, 0.0, 0.0 }; foreach (var edgeVector in edgeVectors) { axis = axis.add(edgeVector); } Axis = axis.normalize(); /* to adjust the Axis, we will average the cross products of the new face with all the old faces */ //Since we will be taking cross products, we need to be sure not to have faces along the same normal var faces = MiscFunctions.FacesWithDistinctNormals(Faces.ToList()); var n = faces.Count; //Check if the loops are circular along the axis var path1 = MiscFunctions.Get2DProjectionPointsAsLight(Loop1, Axis, out var backTransform); var poly = new PolygonLight(path1); if (!PolygonOperations.IsCircular(new Polygon(poly), out var centerCircle, Constants.MediumConfidence)) { return(false); } var path2 = MiscFunctions.Get2DProjectionPointsAsLight(Loop2, Axis, out var backTransform2); var poly2 = new PolygonLight(path2); if (!PolygonOperations.IsCircular(new Polygon(poly2), out var centerCircle2, Constants.MediumConfidence)) { return(false); } Radius = (centerCircle.Radius + centerCircle2.Radius) / 2; //Average //Set the Anchor/Center point var center = centerCircle.Center.Add(centerCircle2.Center).divide(2); Anchor = MiscFunctions.Convert2DVectorTo3DVector(center, backTransform); return(true); }
/// <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); }