using System; using System.Collections.Generic; using OpenNest.CNC; using OpenNest.Geometry; namespace OpenNest.Posts.CincinnatiCIFiber { /// /// Works out, from a part's flattened contours, which one is the perimeter /// (L4 lead-in macro, the rest are holes on L2) and which compensation /// side keeps the kerf in the scrap. /// /// G41 leaves the product right of travel. Imported drawings are wound /// perimeter CW and holes CCW (ShapeProfile.NormalizeWinding), which puts /// the part on the right and gives G41 throughout; a drawing that reaches /// the post wound the other way (built in code or edited by hand) needs G42. /// Paths that enclose no area (open break cuts) count as holes with G41, /// a defensive default. /// 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; } /// /// Index of the part's perimeter: the contour enclosing the largest /// bounding box, as ShapeProfile picks it. -1 when no contour 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 (!EnclosesArea(contours[i])) continue; var area = BoundingBoxArea(SamplePoints(contours[i])); if (best < 0 || area > bestArea) { best = i; bestArea = area; } } return best; } /// /// True when G41 (product right of travel) keeps the kerf in the scrap: /// a CW perimeter or a CCW hole. False calls for G42. /// public static bool CompensatesLeft(CIFiberContour contour, bool isPerimeter) { if (!EnclosesArea(contour)) return true; var clockwise = SignedArea(contour) < 0.0; return clockwise == isPerimeter; } private static bool EnclosesArea(CIFiberContour 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); } /// 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; } } }