using System.Collections.Generic;
using OpenNest.Geometry;
using OpenNest.Math;
namespace OpenNest.CNC.Posting;
///
/// Which posted contour is the perimeter, and which compensation side keeps the kerf in the
/// scrap. Imported drawings are wound perimeter CW and holes CCW
/// (), which puts the part right of travel and gives
/// G41 throughout; a contour wound the other way (built in code or edited by hand) needs G42.
/// Paths that enclose no area (open break cuts) get G41, a defensive default.
///
public static class ContourWinding
{
/// Signed shoelace area of the cut path; negative when clockwise, 0 when degenerate.
public static double SignedArea(PostContour contour)
{
var pts = SamplePoints(contour);
if (pts.Count < 3)
return 0.0;
var sum = 0.0;
for (var i = 0; i < pts.Count; i++)
{
var a = pts[i];
var b = pts[(i + 1) % pts.Count];
sum += a.X * b.Y - b.X * a.Y;
}
return sum / 2.0;
}
///
/// Index of the perimeter: the material contour enclosing the largest bounding box, as
/// picks it. -1 when none encloses any area.
///
public static int PerimeterIndex(IReadOnlyList contours)
{
var best = -1;
var bestArea = 0.0;
for (var i = 0; i < contours.Count; i++)
{
if (contours[i].IsMark || !EnclosesArea(contours[i]))
continue;
var area = BoundingBoxArea(SamplePoints(contours[i]));
if (best < 0 || area > bestArea)
{
best = i;
bestArea = area;
}
}
return best;
}
///
/// (G41, part right of travel) for a CW perimeter, a CCW hole or
/// a path enclosing no area; otherwise (G42).
///
public static KerfType CompensationSide(PostContour contour, bool isPerimeter)
{
if (!EnclosesArea(contour))
return KerfType.Left;
var clockwise = SignedArea(contour) < 0.0;
return clockwise == isPerimeter ? KerfType.Left : KerfType.Right;
}
private static bool EnclosesArea(PostContour contour) =>
System.Math.Abs(SignedArea(contour)) > 1e-9;
private static double BoundingBoxArea(List pts)
{
double minX = double.MaxValue, minY = double.MaxValue;
double maxX = double.MinValue, maxY = double.MinValue;
foreach (var p in pts)
{
minX = System.Math.Min(minX, p.X);
minY = System.Math.Min(minY, p.Y);
maxX = System.Math.Max(maxX, p.X);
maxY = System.Math.Max(maxY, p.Y);
}
return (maxX - minX) * (maxY - minY);
}
///
/// Ordered points along the path: the contour start, then each cut endpoint, with arc
/// interior samples at no more than 90-degree steps so full circles give a well-formed
/// polygon. Consecutive duplicates are dropped so zero-length lead-ins do not skew it.
///
private static List SamplePoints(PostContour contour)
{
var pts = new List();
void Add(Vector v)
{
if (pts.Count == 0 || pts[^1].DistanceTo(v) > 1e-9)
pts.Add(v);
}
var prev = contour.LeadIn != null ? contour.LeadIn.EndPoint : contour.Pierce;
Add(prev);
foreach (var cut in contour.Cuts)
{
if (cut is ArcMove arc)
{
foreach (var mid in ArcSamplePoints(prev, arc))
Add(mid);
}
Add(cut.EndPoint);
prev = cut.EndPoint;
}
return pts;
}
///
/// Interior points on the arc from to its endpoint at no more than
/// ~90-degree steps. A full circle yields four, so its shoelace sign is its true winding.
///
private static List ArcSamplePoints(Vector prev, ArcMove arc)
{
var points = new List();
var center = arc.CenterPoint;
var radius = center.DistanceTo(arc.EndPoint);
if (radius < 1e-12)
return points;
var a0 = PortableMath.Atan2(prev.Y - center.Y, prev.X - center.X);
var a1 = PortableMath.Atan2(arc.EndPoint.Y - center.Y, arc.EndPoint.X - center.X);
var sweep = arc.Rotation == RotationType.CW ? a0 - a1 : a1 - a0;
if (sweep <= 0)
sweep += 2.0 * System.Math.PI;
// Full circle (start == end) reads as zero sweep above.
if (
sweep < 1e-9
&& System.Math.Abs(prev.X - arc.EndPoint.X) < 1e-9
&& System.Math.Abs(prev.Y - arc.EndPoint.Y) < 1e-9
)
sweep = 2.0 * System.Math.PI;
var direction = arc.Rotation == RotationType.CW ? -1.0 : 1.0;
var steps = System.Math.Max(1, (int)System.Math.Ceiling(sweep / (System.Math.PI / 2.0)));
for (var k = 1; k <= steps; k++)
{
var angle = a0 + direction * sweep * k / (steps + 1);
points.Add(
new Vector(
center.X + radius * PortableMath.Cos(angle),
center.Y + radius * PortableMath.Sin(angle)
)
);
}
return points;
}
}