Its callers now take Clipper region offsets, which never produce the self-intersections it patched over (and it only caught proper crossings, so spikes survived it anyway). Polygon.OffsetEntity was its last caller; the override is required by Entity but has no callers, so it becomes a Clipper miter offset that keeps the Left/Right semantics and the input winding. FindCrossing, SplitAtCrossing, SegmentsIntersect and the static CalculateArea helper go with it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
572 lines
16 KiB
C#
572 lines
16 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using OpenNest.Math;
|
|
|
|
namespace OpenNest.Geometry
|
|
{
|
|
public class Polygon : Entity
|
|
{
|
|
public List<Vector> Vertices;
|
|
|
|
public Polygon()
|
|
{
|
|
Vertices = new List<Vector>();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Closes the polygon if it's not already.
|
|
/// </summary>
|
|
public void Close()
|
|
{
|
|
if (Vertices.Count < 3)
|
|
return;
|
|
|
|
var first = Vertices.First();
|
|
var last = Vertices.Last();
|
|
|
|
if (first != last)
|
|
Vertices.Add(first);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true if the polygon is closed.
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
public bool IsClosed()
|
|
{
|
|
if (Vertices.Count < 3)
|
|
return false;
|
|
|
|
return (Vertices.First() == Vertices.Last());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true if the polygon is self intersecting.
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Area of the polygon.
|
|
/// </summary>
|
|
/// <returns>Returns the area or 0 if the polygon is NOT closed.</returns>
|
|
public double Area()
|
|
{
|
|
if (Vertices.Count < 3)
|
|
return 0.0;
|
|
|
|
return System.Math.Abs(CalculateArea());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Distance around the polygon.
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the rotation direction of the polygon.
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Converts the polygon to a group of lines.
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
public List<Line> ToLines()
|
|
{
|
|
var list = new List<Line>();
|
|
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the area of the polygon.
|
|
/// </summary>
|
|
/// <returns>
|
|
/// Returns the area of the polygon.
|
|
/// * Positive number = counter-clockwise rotation
|
|
/// * Negative number = clockwise rotation
|
|
/// </returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Distance around the polygon.
|
|
/// </summary>
|
|
public override double Length
|
|
{
|
|
get { return Perimeter(); }
|
|
}
|
|
|
|
public override Entity Clone()
|
|
{
|
|
var copy = new Polygon { Vertices = new List<Vector>(Vertices) };
|
|
CopyBaseTo(copy);
|
|
return copy;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reverses the rotation direction of the polygon.
|
|
/// </summary>
|
|
public override void Reverse()
|
|
{
|
|
Vertices.Reverse();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Moves the start point to the given coordinates.
|
|
/// </summary>
|
|
/// <param name="x"></param>
|
|
/// <param name="y"></param>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Moves the start point to the given point.
|
|
/// </summary>
|
|
/// <param name="pt"></param>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Offsets the location by the given distances.
|
|
/// </summary>
|
|
/// <param name="x"></param>
|
|
/// <param name="y"></param>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Offsets the location by the given distances.
|
|
/// </summary>
|
|
/// <param name="voffset"></param>
|
|
public override void Offset(Vector voffset)
|
|
{
|
|
for (int i = 0; i < Vertices.Count; i++)
|
|
Vertices[i] = Vertices[i].Offset(voffset);
|
|
|
|
boundingBox.Offset(voffset);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Scales the polygon from the zero point.
|
|
/// </summary>
|
|
/// <param name="factor"></param>
|
|
public override void Scale(double factor)
|
|
{
|
|
for (int i = 0; i < Vertices.Count; i++)
|
|
Vertices[i] *= factor;
|
|
|
|
UpdateBounds();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Scales the polygon from the zero point.
|
|
/// </summary>
|
|
/// <param name="factor"></param>
|
|
/// <param name="origin"></param>
|
|
public override void Scale(double factor, Vector origin)
|
|
{
|
|
for (int i = 0; i < Vertices.Count; i++)
|
|
Vertices[i] = (Vertices[i] - origin) * factor + origin;
|
|
|
|
UpdateBounds();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Rotates the polygon from the zero point.
|
|
/// </summary>
|
|
/// <param name="angle"></param>
|
|
public override void Rotate(double angle)
|
|
{
|
|
for (int i = 0; i < Vertices.Count; i++)
|
|
Vertices[i] = Vertices[i].Rotate(angle);
|
|
|
|
UpdateBounds();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Rotates the polygon from the origin.
|
|
/// </summary>
|
|
/// <param name="angle"></param>
|
|
/// <param name="origin"></param>
|
|
public override void Rotate(double angle, Vector origin)
|
|
{
|
|
for (int i = 0; i < Vertices.Count; i++)
|
|
Vertices[i] = Vertices[i].Rotate(angle, origin);
|
|
|
|
UpdateBounds();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the bounding box.
|
|
/// </summary>
|
|
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.Length = maxX - minX;
|
|
boundingBox.Width = maxY - minY;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Miter-offsets the closed polygon to the given side, keeping its winding.
|
|
/// Corners sharper than the miter limit are squared off, and features that
|
|
/// collapse under the offset are dropped. When the offset splits the polygon,
|
|
/// the largest piece is returned.
|
|
/// </summary>
|
|
public override Entity OffsetEntity(double distance, OffsetSide side)
|
|
{
|
|
if (Vertices.Count < 3)
|
|
return null;
|
|
|
|
var ccw = CalculateArea() > 0;
|
|
var outward = ccw ? OffsetSide.Left : OffsetSide.Right;
|
|
var delta = side == outward ? distance : -distance;
|
|
|
|
var result = ClipperBridge.OffsetMiter(this, delta);
|
|
|
|
if (result == null)
|
|
return null;
|
|
|
|
if (!ccw)
|
|
result.Reverse();
|
|
|
|
return result;
|
|
}
|
|
|
|
public override Entity OffsetEntity(double distance, Vector pt)
|
|
{
|
|
var left = OffsetEntity(distance, OffsetSide.Left);
|
|
var right = OffsetEntity(distance, OffsetSide.Right);
|
|
|
|
if (left == null)
|
|
return right;
|
|
if (right == null)
|
|
return left;
|
|
|
|
var distLeft = left.ClosestPointTo(pt).DistanceTo(pt);
|
|
var distRight = right.ClosestPointTo(pt).DistanceTo(pt);
|
|
|
|
return distLeft > distRight ? left : right;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the closest point on the polygon to the given point.
|
|
/// </summary>
|
|
/// <param name="pt"></param>
|
|
/// <returns></returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true if the given arc is intersecting this.
|
|
/// </summary>
|
|
/// <param name="arc"></param>
|
|
/// <returns></returns>
|
|
public override bool Intersects(Arc arc)
|
|
{
|
|
List<Vector> pts;
|
|
return Intersect.Intersects(arc, this, out pts);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true if the given arc is intersecting this.
|
|
/// </summary>
|
|
/// <param name="arc"></param>
|
|
/// <param name="pts"></param>
|
|
/// <returns></returns>
|
|
public override bool Intersects(Arc arc, out List<Vector> pts)
|
|
{
|
|
return Intersect.Intersects(arc, this, out pts);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true if the given circle is intersecting this.
|
|
/// </summary>
|
|
/// <param name="circle"></param>
|
|
/// <returns></returns>
|
|
public override bool Intersects(Circle circle)
|
|
{
|
|
List<Vector> pts;
|
|
return Intersect.Intersects(circle, this, out pts);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true if the given circle is intersecting this.
|
|
/// </summary>
|
|
/// <param name="circle"></param>
|
|
/// <param name="pts"></param>
|
|
/// <returns></returns>
|
|
public override bool Intersects(Circle circle, out List<Vector> pts)
|
|
{
|
|
return Intersect.Intersects(circle, this, out pts);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true if the given line is intersecting this.
|
|
/// </summary>
|
|
/// <param name="line"></param>
|
|
/// <returns></returns>
|
|
public override bool Intersects(Line line)
|
|
{
|
|
List<Vector> pts;
|
|
return Intersect.Intersects(line, this, out pts);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true if the given line is intersecting this.
|
|
/// </summary>
|
|
/// <param name="line"></param>
|
|
/// <param name="pts"></param>
|
|
/// <returns></returns>
|
|
public override bool Intersects(Line line, out List<Vector> pts)
|
|
{
|
|
return Intersect.Intersects(line, this, out pts);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true if the given polygon is intersecting this.
|
|
/// </summary>
|
|
/// <param name="polygon"></param>
|
|
/// <returns></returns>
|
|
public override bool Intersects(Polygon polygon)
|
|
{
|
|
List<Vector> pts;
|
|
return Intersect.Intersects(this, polygon, out pts);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true if the given polygon is intersecting this.
|
|
/// </summary>
|
|
/// <param name="polygon"></param>
|
|
/// <param name="pts"></param>
|
|
/// <returns></returns>
|
|
public override bool Intersects(Polygon polygon, out List<Vector> pts)
|
|
{
|
|
return Intersect.Intersects(this, polygon, out pts);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true if the given shape is intersecting this.
|
|
/// </summary>
|
|
/// <param name="shape"></param>
|
|
/// <returns></returns>
|
|
public override bool Intersects(Shape shape)
|
|
{
|
|
List<Vector> pts;
|
|
return Intersect.Intersects(shape, this, out pts);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true if the given shape is intersecting this.
|
|
/// </summary>
|
|
/// <param name="shape"></param>
|
|
/// <param name="pts"></param>
|
|
/// <returns></returns>
|
|
public override bool Intersects(Shape shape, out List<Vector> pts)
|
|
{
|
|
return Intersect.Intersects(shape, this, out pts);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Type of entity.
|
|
/// </summary>
|
|
public override EntityType Type
|
|
{
|
|
get { return EntityType.Polygon; }
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|