using System; using System.Collections.Generic; using OpenNest.CNC; using OpenNest.Geometry; namespace OpenNest.Posts.CincinnatiCIFiber { /// /// Classifies a flattened contour as interior or exterior from the /// material side of its cut path, so the post picks the G41 (inside, kerf /// left of travel) vs G42 (outside, kerf right of travel) lead-in macro to /// keep the kerf on the scrap side, matching the machine sample (holes cut /// CCW + G41, perimeter cut CCW + G42). /// /// OpenNest contours all keep material on the left of travel (see /// OffsetSide / ContourCuttingStrategy): the perimeter runs clockwise /// (negative shoelace area) and holes run counter-clockwise (positive). /// A contour path that is not closed classifies as interior — a defensive /// default; open paths should not reach the post. /// public static class CIFiberWinding { /// /// Signed shoelace area of the contour path (arcs sampled at their /// arc-mid point). 0 for degenerate paths. /// public static double SignedArea(CIFiberContour 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; } /// /// True when the contour is a closed CW path — an external perimeter /// under the OpenNest material-left convention. /// public static bool IsExterior(CIFiberContour contour) { if (!IsClosed(contour)) return false; return SignedArea(contour) < 0.0; } /// True when the cut path returns to its start point. public static bool IsClosed(CIFiberContour contour, double tolerance = 1e-6) { var pts = SamplePoints(contour); if (pts.Count < 3) return false; return pts[0].DistanceTo(pts[^1]) <= tolerance; } /// /// Ordered points along the path: lead-in endpoint, then each cut move /// endpoint, with arc interior samples at <=90-degree steps (so full /// circles produce a well-formed polygon). Consecutive duplicates are /// dropped so zero-length lead-ins do not skew the sample. /// public static List SamplePoints(CIFiberContour 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 the arc /// endpoint at no more than ~90-degree sweep intervals. A full circle /// yields four interior points, giving the shoelace a non-degenerate /// polygon whose sign is the circle's true winding. /// public 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 = System.Math.Atan2(prev.Y - center.Y, prev.X - center.X); var a1 = System.Math.Atan2(arc.EndPoint.Y - center.Y, arc.EndPoint.X - center.X); double sweep; if (arc.Rotation == RotationType.CW) { sweep = a0 - a1; if (sweep <= 0) sweep += 2.0 * System.Math.PI; } else { sweep = 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 * System.Math.Cos(angle), center.Y + radius * System.Math.Sin(angle) ) ); } return points; } } }