using System; using System.Collections.Generic; using System.Linq; using OpenNest.Math; namespace OpenNest.Geometry { public class Polygon : Entity { public List Vertices; public Polygon() { Vertices = new List(); } /// /// Closes the polygon if it's not already. /// public void Close() { if (Vertices.Count < 3) return; var first = Vertices.First(); var last = Vertices.Last(); if (first != last) Vertices.Add(first); } /// /// Returns true if the polygon is closed. /// /// public bool IsClosed() { if (Vertices.Count < 3) return false; return (Vertices.First() == Vertices.Last()); } /// /// Returns true if the polygon is self intersecting. /// /// public bool IsComplex() { var lines = ToLines(); for (int i = 0; i < lines.Count; ++i) { var line1 = lines[i]; for (int j = i; j < lines.Count; ++j) { var line2 = lines[j]; if (line1.Intersects(line2)) return true; } } return false; } /// /// Area of the polygon. /// /// Returns the area or 0 if the polygon is NOT closed. public double Area() { if (Vertices.Count < 3) return 0.0; return System.Math.Abs(CalculateArea()); } /// /// Distance around the polygon. /// /// public double Perimeter() { if (Vertices.Count < 3) return 0.0; double sum = 0.0; var last = Vertices[0]; for (int i = 1; i < Vertices.Count; ++i) { var current = Vertices[i]; sum += last.DistanceTo(current); last = current; } return sum; } /// /// Gets the rotation direction of the polygon. /// /// public RotationType RotationDirection() { if (Vertices.Count < 3) throw new Exception("Not enough points to determine direction. Must have at least 3 points."); return CalculateArea() > 0 ? RotationType.CCW : RotationType.CW; } /// /// Converts the polygon to a group of lines. /// /// public List ToLines() { var list = new List(); if (Vertices.Count < 2) return list; var last = Vertices[0]; for (int i = 1; i < Vertices.Count; ++i) { var current = Vertices[i]; list.Add(new Line(last, current)); last = current; } return list; } /// /// Gets the area of the polygon. /// /// /// Returns the area of the polygon. /// * Positive number = counter-clockwise rotation /// * Negative number = clockwise rotation /// private double CalculateArea() { double xsum = 0; double ysum = 0; for (int i = 0; i < Vertices.Count - 1; ++i) { var current = Vertices[i]; var next = Vertices[i + 1]; xsum += current.X * next.Y; ysum += current.Y * next.X; } return (xsum - ysum) * 0.5; } /// /// Distance around the polygon. /// public override double Length { get { return Perimeter(); } } /// /// Reverses the rotation direction of the polygon. /// public override void Reverse() { Vertices.Reverse(); } /// /// Moves the start point to the given coordinates. /// /// /// public override void MoveTo(double x, double y) { if (Vertices.Count == 0) return; var first = Vertices[0]; var offset = new Vector(x - first.X, y - first.Y); Vertices.ForEach(vertex => vertex += offset); boundingBox.Offset(offset); } /// /// Moves the start point to the given point. /// /// public override void MoveTo(Vector pt) { if (Vertices.Count == 0) return; var first = Vertices[0]; var offset = pt - first; Vertices.ForEach(vertex => vertex += offset); boundingBox.Offset(offset); } /// /// Offsets the location by the given distances. /// /// /// public override void Offset(double x, double y) { for (int i = 0; i < Vertices.Count; i++) Vertices[i] = Vertices[i].Offset(x, y); boundingBox.Offset(x, y); } /// /// Offsets the location by the given distances. /// /// public override void Offset(Vector voffset) { for (int i = 0; i < Vertices.Count; i++) Vertices[i] = Vertices[i].Offset(voffset); boundingBox.Offset(voffset); } /// /// Scales the polygon from the zero point. /// /// public override void Scale(double factor) { for (int i = 0; i < Vertices.Count; i++) Vertices[i] *= factor; UpdateBounds(); } /// /// Scales the polygon from the zero point. /// /// /// public override void Scale(double factor, Vector origin) { for (int i = 0; i < Vertices.Count; i++) Vertices[i] = (Vertices[i] - origin) * factor + origin; UpdateBounds(); } /// /// Rotates the polygon from the zero point. /// /// public override void Rotate(double angle) { for (int i = 0; i < Vertices.Count; i++) Vertices[i] = Vertices[i].Rotate(angle); UpdateBounds(); } /// /// Rotates the polygon from the origin. /// /// /// public override void Rotate(double angle, Vector origin) { for (int i = 0; i < Vertices.Count; i++) Vertices[i] = Vertices[i].Rotate(angle, origin); UpdateBounds(); } /// /// Updates the bounding box. /// public override void UpdateBounds() { if (Vertices.Count == 0) return; var first = Vertices[0]; var minX = first.X; var maxX = first.X; var minY = first.Y; var maxY = first.Y; for (int i = 1; i < Vertices.Count; ++i) { var vertex = Vertices[i]; if (vertex.X < minX) minX = vertex.X; else if (vertex.X > maxX) maxX = vertex.X; if (vertex.Y < minY) minY = vertex.Y; else if (vertex.Y > maxY) maxY = vertex.Y; } boundingBox.X = minX; boundingBox.Y = minY; boundingBox.Width = maxX - minX; boundingBox.Length = maxY - minY; } public override Entity OffsetEntity(double distance, OffsetSide side) { throw new NotImplementedException(); } public override Entity OffsetEntity(double distance, Vector pt) { throw new NotImplementedException(); } /// /// Gets the closest point on the polygon to the given point. /// /// /// public override Vector ClosestPointTo(Vector pt) { var lines = ToLines(); if (lines.Count == 0) return Vector.Invalid; Vector closestPt = lines[0].ClosestPointTo(pt); double distance = closestPt.DistanceTo(pt); for (int i = 1; i < lines.Count; i++) { var line = lines[i]; var closestPt2 = line.ClosestPointTo(pt); var distance2 = closestPt2.DistanceTo(pt); if (distance2 < distance) { closestPt = closestPt2; distance = distance2; } } return closestPt; } /// /// Returns true if the given arc is intersecting this. /// /// /// public override bool Intersects(Arc arc) { List pts; return Intersect.Intersects(arc, this, out pts); } /// /// Returns true if the given arc is intersecting this. /// /// /// /// public override bool Intersects(Arc arc, out List pts) { return Intersect.Intersects(arc, this, out pts); } /// /// Returns true if the given circle is intersecting this. /// /// /// public override bool Intersects(Circle circle) { List pts; return Intersect.Intersects(circle, this, out pts); } /// /// Returns true if the given circle is intersecting this. /// /// /// /// public override bool Intersects(Circle circle, out List pts) { return Intersect.Intersects(circle, this, out pts); } /// /// Returns true if the given line is intersecting this. /// /// /// public override bool Intersects(Line line) { List pts; return Intersect.Intersects(line, this, out pts); } /// /// Returns true if the given line is intersecting this. /// /// /// /// public override bool Intersects(Line line, out List pts) { return Intersect.Intersects(line, this, out pts); } /// /// Returns true if the given polygon is intersecting this. /// /// /// public override bool Intersects(Polygon polygon) { List pts; return Intersect.Intersects(this, polygon, out pts); } /// /// Returns true if the given polygon is intersecting this. /// /// /// /// public override bool Intersects(Polygon polygon, out List pts) { return Intersect.Intersects(this, polygon, out pts); } /// /// Returns true if the given shape is intersecting this. /// /// /// public override bool Intersects(Shape shape) { List pts; return Intersect.Intersects(shape, this, out pts); } /// /// Returns true if the given shape is intersecting this. /// /// /// /// public override bool Intersects(Shape shape, out List pts) { return Intersect.Intersects(shape, this, out pts); } /// /// Type of entity. /// public override EntityType Type { get { return EntityType.Polygon; } } /// /// Removes self-intersecting loops from the polygon by finding non-adjacent /// edge crossings and keeping the larger contour at each crossing. /// public void RemoveSelfIntersections() { if (!IsClosed() || Vertices.Count < 5) return; while (FindCrossing(out var edgeI, out var edgeJ, out var pt)) { Vertices = SplitAtCrossing(edgeI, edgeJ, pt); } } private bool FindCrossing(out int edgeI, out int edgeJ, out Vector pt) { var n = Vertices.Count - 1; // Pre-calculate edge bounding boxes to speed up intersection checks. var edgeBounds = new (double minX, double maxX, double minY, double maxY)[n]; for (var i = 0; i < n; i++) { var v1 = Vertices[i]; var v2 = Vertices[i + 1]; edgeBounds[i] = ( System.Math.Min(v1.X, v2.X) - Tolerance.Epsilon, System.Math.Max(v1.X, v2.X) + Tolerance.Epsilon, System.Math.Min(v1.Y, v2.Y) - Tolerance.Epsilon, System.Math.Max(v1.Y, v2.Y) + Tolerance.Epsilon ); } for (var i = 0; i < n; i++) { var bi = edgeBounds[i]; for (var j = i + 2; j < n; j++) { if (i == 0 && j == n - 1) continue; var bj = edgeBounds[j]; // Prune with bounding box check. if (bi.maxX < bj.minX || bj.maxX < bi.minX || bi.maxY < bj.minY || bj.maxY < bi.minY) { continue; } if (SegmentsIntersect(Vertices[i], Vertices[i + 1], Vertices[j], Vertices[j + 1], out pt)) { edgeI = i; edgeJ = j; return true; } } } edgeI = edgeJ = -1; pt = Vector.Zero; return false; } private List SplitAtCrossing(int edgeI, int edgeJ, Vector pt) { var n = Vertices.Count - 1; var loopA = Vertices.GetRange(0, edgeI + 1); loopA.Add(pt); loopA.AddRange(Vertices.GetRange(edgeJ + 1, n - edgeJ - 1)); loopA.Add(loopA[0]); var loopB = new List { pt }; loopB.AddRange(Vertices.GetRange(edgeI + 1, edgeJ - edgeI)); loopB.Add(pt); var areaA = System.Math.Abs(CalculateArea(loopA)); var areaB = System.Math.Abs(CalculateArea(loopB)); return areaA >= areaB ? loopA : loopB; } private static bool SegmentsIntersect(Vector a1, Vector a2, Vector b1, Vector b2, out Vector pt) { var da = a2 - a1; var db = b2 - b1; var cross = da.X * db.Y - da.Y * db.X; if (cross.IsEqualTo(0.0)) { pt = Vector.Zero; return false; } var dc = b1 - a1; var t = (dc.X * db.Y - dc.Y * db.X) / cross; var u = (dc.X * da.Y - dc.Y * da.X) / cross; if (t > Tolerance.Epsilon && t < 1.0 - Tolerance.Epsilon && u > Tolerance.Epsilon && u < 1.0 - Tolerance.Epsilon) { pt = new Vector(a1.X + t * da.X, a1.Y + t * da.Y); return true; } pt = Vector.Zero; return false; } private static double CalculateArea(List vertices) { double xsum = 0; double ysum = 0; for (int i = 0; i < vertices.Count - 1; i++) { var current = vertices[i]; var next = vertices[i + 1]; xsum += current.X * next.Y; ysum += current.Y * next.X; } return (xsum - ysum) * 0.5; } internal void Cleanup() { for (int i = Vertices.Count - 1; i > 0; i--) { var vertex = Vertices[i]; var nextVertex = Vertices[i - 1]; if (vertex == nextVertex) Vertices.RemoveAt(i); } } public BoundingRectangleResult FindBestRotation() { return RotatingCalipers.MinimumBoundingRectangle(Vertices); } public BoundingRectangleResult FindBestRotation(double startAngle, double endAngle) { var hull = ConvexHull.Compute(Vertices); return RotatingCalipers.MinimumBoundingRectangle(hull, startAngle, endAngle); } public bool ContainsPoint(Vector pt) { var n = IsClosed() ? Vertices.Count - 1 : Vertices.Count; if (n < 3) return false; var inside = false; for (int i = 0, j = n - 1; i < n; j = i++) { var vi = Vertices[i]; var vj = Vertices[j]; if ((vi.Y > pt.Y) != (vj.Y > pt.Y) && pt.X < (vj.X - vi.X) * (pt.Y - vi.Y) / (vj.Y - vi.Y) + vi.X) { inside = !inside; } } return inside; } } }