/// <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(); }
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(); }
/// <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); }
private static IEnumerable <List <PointLight> > GetSurfacePaths(List <HashSet <PolygonalFace> > surfaces, double[] normal, double minAreaToConsider, TessellatedSolid originalSolid, Dictionary <int, List <PointLight> > projectedFacePolygons) { originalSolid.HasUniformColor = false; var red = new Color(KnownColors.Red); var allPaths = new List <List <PointLight> >(); foreach (var surface in surfaces) { //Get the surface inner and outer edges var outerEdges = new HashSet <Edge>(); var innerEdges = new HashSet <Edge>(); foreach (var face in surface) { if (face.Edges.Count != 3) { throw new Exception(); } foreach (var edge in face.Edges) { //if (innerEdges.Contains(edge)) continue; if (!outerEdges.Contains(edge)) { outerEdges.Add(edge); } else if (outerEdges.Contains(edge)) { innerEdges.Add(edge); outerEdges.Remove(edge); } else { throw new Exception(); } } } var surfacePaths = new List <List <PointLight> >(); var assignedEdges = new HashSet <Edge>(); while (outerEdges.Any()) { //Get the start vertex and edge and save them to the lists var startEdge = outerEdges.First(); var startVertex = startEdge.From; var loop = new List <Vertex> { startVertex }; var nextVertex = startEdge.To; var edgeLoop = new List <(Edge, Vertex, Vertex)> { (startEdge, startVertex, nextVertex) }; assignedEdges.Add(startEdge); //Initialize the current vertex and edge var vertex = startEdge.From; var currentEdge = startEdge; //Loop until back to the start vertex while (nextVertex.IndexInList != startVertex.IndexInList) { //Get the next edge var nextEdges = nextVertex.Edges.Where(e => !assignedEdges.Contains(e)).Where(e => outerEdges.Contains(e)) .ToList(); if (nextEdges.Count == 0) { Debug.WriteLine("Surface paths do not wrap around properly. Artificially closing loop."); break; } if (nextEdges.Count > 1) { //There are multiple edges to go to next. Simply reversing will cause an issue //if the same thing happens along the other direction. //To avoid this, we go in the direction of the current edge's surface face, until we //hit an edge. var minAngle = 2 * Math.PI; var currentFace = surface.Contains(currentEdge.OtherFace) ? currentEdge.OtherFace : currentEdge.OwnedFace; //currentFace.Color = new Color(KnownColors.White); var otherVertex = currentFace.OtherVertex(currentEdge.To, currentEdge.From); var angle1 = MiscFunctions.ProjectedExteriorAngleBetweenVerticesCCW(vertex, nextVertex, otherVertex, normal); var angle2 = MiscFunctions.ProjectedInteriorAngleBetweenVerticesCCW(vertex, nextVertex, otherVertex, normal); if (angle1 < angle2) { //Use the exterior angle foreach (var edge in nextEdges) { var furtherVertex = edge.OtherVertex(nextVertex); var angle = MiscFunctions.ProjectedExteriorAngleBetweenVerticesCCW(vertex, nextVertex, furtherVertex, normal); if (!(angle < minAngle)) { continue; } minAngle = angle; //Update the current edge currentEdge = edge; } } else { //Use the interior angle foreach (var edge in nextEdges) { var furtherVertex = edge.OtherVertex(nextVertex); var angle = MiscFunctions.ProjectedInteriorAngleBetweenVerticesCCW(vertex, nextVertex, furtherVertex, normal); if (!(angle < minAngle)) { continue; } minAngle = angle; //Update the current edge currentEdge = edge; PolygonalFace faceInQuestion = null; if (surface.Contains(edge.OwnedFace) && surface.Contains(edge.OtherFace)) { //edge.OwnedFace.Color = new Color(KnownColors.Green); //edge.OtherFace.Color = red; //Presenter.ShowVertexPathsWithSolid(new List<List<List<Vertex>>> { error2Loops }, // new List<TessellatedSolid> { originalSolid }); } else if (surface.Contains(edge.OwnedFace)) { faceInQuestion = edge.OtherFace; } else if (surface.Contains(edge.OtherFace)) { faceInQuestion = edge.OwnedFace; } if (faceInQuestion != null) { //faceInQuestion.Color = red; //var n2 = PolygonalFace.DetermineNormal(faceInQuestion.Vertices, out _); //Presenter.ShowAndHang(originalSolid); //Presenter.ShowVertexPathsWithSolid(new List<List<List<Vertex>>> { error2Loops }, // new List<TessellatedSolid> { originalSolid }); } } } foreach (var edge in nextEdges) { if (currentFace.Edges.Contains(edge)) { if (edge == currentEdge) { break; //This is what we want } //Presenter.ShowVertexPathsWithSolid(new List<List<List<Vertex>>> { error2Loops }, // new List<TessellatedSolid> { originalSolid }); } } } else { //Update the current edge currentEdge = nextEdges.First(); } //Update the current vertex vertex = nextVertex; loop.Add(vertex); //Get the next vertex nextVertex = currentEdge.OtherVertex(vertex); edgeLoop.Add((currentEdge, vertex, nextVertex)); assignedEdges.Add(currentEdge); } //To determine order: //The vertices should be listed such that their edge vector cross producted with the second point, //toward a third point on the positive face that provided this edge lines up with the normal. //If that is incorrect, then this may be a hole. //All edges should agree on this test. var correct = 0; var needsReversal = 0; foreach (var edgeTuple in edgeLoop) { var edge = edgeTuple.Item1; outerEdges.Remove(edge); var isOtherFace = surface.Contains(edge.OtherFace); var isOwnedFace = surface.Contains(edge.OwnedFace); if (isOwnedFace == isOtherFace) { throw new Exception("Should be one and only one face for this edge on this surface"); } var positiveFaceBelongingToEdge = isOwnedFace ? edge.OwnedFace : edge.OtherFace; var vertex3 = positiveFaceBelongingToEdge.OtherVertex(edge); var v1 = vertex3.Position.subtract(edgeTuple.Item3.Position, 3); //To point according to our loop var v2 = edgeTuple.Item3.Position.subtract(edgeTuple.Item2.Position, 3); //To minus from var dot = v2.crossProduct(v1).dotProduct(positiveFaceBelongingToEdge.Normal, 3); if (dot > 0) { correct++; } else { needsReversal++; } } if (needsReversal > correct) { loop.Reverse(); } //if(needsReversal*correct != 0) Debug.WriteLine("Reversed Loop Count: " + needsReversal + " Forward Loop Count: " + correct); //Get2DProjections does not project directionally (normal and normal.multiply(-1) return the same transform) //However, the way we are unioning the polygons and eliminating overhand polygons seems to be taking care of this var surfacePath = MiscFunctions.Get2DProjectionPointsAsLight(loop, normal).ToList(); var area2D = MiscFunctions.AreaOfPolygon(surfacePath); if (area2D.IsNegligible(minAreaToConsider)) { continue; } //Trust the ordering from the face normals. A self intersecting polygon may have a negative area, //but in-fact be positive once it undergoes a Fill Positive union. Same goes for positive areas. //if (Math.Sign(area2D) != Math.Sign(area3D)) surfacePath.Reverse(); surfacePaths.Add(surfacePath); } if (!surfacePaths.Any()) { continue; } allPaths.AddRange(surfacePaths); } //By unioning the path into non-self intersecting paths, //partially covered holes will be reduced to their final non-covered size. //This is necessary for the next few checks in determining if it is a hole or an overhang. //This union operation is the trickiest union in the silhouette function to reason through. //Using positive fill or even/odd perform pretty well, but they union overlapping //negative regions. This is undesirable, since we do not want to union a hole //with an overlapping region. For this reason, Union Non-Zero is used. It keeps //the holes in their proper orientation and does not combine them together. var nonSelfIntersectingPaths = PolygonOperations.Union(allPaths, false, PolygonFillType.NonZero); var correctedSurfacePath = EliminateOverhangPolygons(nonSelfIntersectingPaths, projectedFacePolygons); //if (allPaths.Sum(p => p.Count) > 10) Presenter.ShowAndHang(nonSelfIntersectingPaths); return(correctedSurfacePath); }
/// <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); }
private double[,] CreateDistanceGrid(IList <Polygon> layer) { var allIntersections = PolygonOperations.AllPolygonIntersectionPointsAlongHorizontalLines(layer, _yMin, numGridY, discretization, out var firstIntersectingIndex); using var allIntersectionsEnumerator = allIntersections.GetEnumerator(); var grid = new double[numGridX, numGridY]; for (int j = 0; j < numGridY; j++) { double[] intersections = null; if (j >= firstIntersectingIndex) { allIntersectionsEnumerator.MoveNext(); intersections = allIntersectionsEnumerator.Current; } if (intersections == null) { for (int i = 0; i < numGridX; i++) { grid[i, j] = double.PositiveInfinity; } } else { var xIndex = 0; var x = _xMin; for (int i = 0; i < numGridX; i++) { while (xIndex < intersections.Length && x > intersections[xIndex]) { xIndex++; } if (xIndex % 2 == 0) { grid[i, j] = double.PositiveInfinity; } else { grid[i, j] = double.NegativeInfinity; } x += discretization; } } } foreach (var polygon in layer) { var numSegments = polygon.Path.Count; var fromPoint = polygon.Path[numSegments - 1]; var lastPoint = polygon.Path[numSegments - 2]; var polygonMinX = polygon.MinX; var polygonMinY = polygon.MinY; var polygonMaxX = polygon.MaxX; var polygonMaxY = polygon.MaxY; var iMin = Math.Max((int)((polygonMinX - _xMin) * coordToGridFactor) - Constants.MarchingCubesBufferFactor, 0); var iMax = Math.Min((int)((polygonMaxX - _xMin) * coordToGridFactor) + Constants.MarchingCubesBufferFactor + 1, numGridX); var jMin = Math.Max((int)((polygonMinY - _yMin) * coordToGridFactor) - Constants.MarchingCubesBufferFactor, 0); var jMax = Math.Min((int)((polygonMaxY - _yMin) * coordToGridFactor) + Constants.MarchingCubesBufferFactor + 1, numGridY); foreach (var toPoint in polygon.Path) { if (Math.Abs(toPoint.Y - fromPoint.Y) > Math.Abs(toPoint.X - fromPoint.X)) { ExpandHorizontally(lastPoint, fromPoint, toPoint, grid, iMin, iMax, jMin, jMax); } else { ExpandVertically(lastPoint, fromPoint, toPoint, grid, iMin, iMax, jMin, jMax); } lastPoint = fromPoint; fromPoint = toPoint; } } return(grid); }
/// <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); }