mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-02 13:41:37 -05:00
Expand Spacing measured every ring pair of every part pair for each trial move, so a packed layout of parts with holes took minutes per spacing probe. Skip ring pairs, far part pairs and off-path parts whose bounds already settle the answer. Every pair was also held at its full starting gap, so parts could not move toward anything and most layouts stayed at their entry spacing. Part pairs may now close to the spacing being tried, never below a smaller starting gap. Cut-offs and clamps keep their full entry gap. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
589 lines
17 KiB
C#
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>(Vertices.Count);
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|