Files
OpenNest/OpenNest.Core/Geometry/Polygon.cs
T
ajandClaude Opus 5.5 9ab6fbbeaf perf(geometry): compact Polygon.Cleanup in one pass
Cleanup removed each duplicate vertex with List.RemoveAt from the middle
of the list, so a polygon cost O(duplicates x vertices). Shape.ToPolygon
emits a duplicate at every entity junction and flattened arcs to 1000
points, so building the Drawing for a large arc-heavy part spent seconds
shifting memory: one 1 MB PEP nest took 12.7 s to convert and 11.2 s to
read back, a quarter of a 300-nest archive import's total time.

The single pass compares each vertex with its original predecessor, as
the back-to-front loop did, so tolerance-equal runs collapse identically
and the output is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 07:13:37 -04:00

589 lines
17 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; }
}
/// <summary>
/// Drops each vertex equal to the one before it in the original sequence. Compacts in
/// place in one pass: removing duplicates one at a time from the middle of the list was
/// O(duplicates x vertices), which dominated loading large drawings whose arcs are
/// flattened to many points.
/// </summary>
internal void Cleanup()
{
if (Vertices.Count < 2)
return;
// Compare against the original predecessor, not the last kept vertex, so a run of
// tolerance-equal points collapses exactly as the old back-to-front RemoveAt loop did.
var previous = Vertices[0];
var kept = 1;
for (int i = 1; i < Vertices.Count; i++)
{
var vertex = Vertices[i];
if (vertex != previous)
Vertices[kept++] = vertex;
previous = vertex;
}
Vertices.RemoveRange(kept, Vertices.Count - kept);
}
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;
}
}
}