using System.Collections.Generic; using OpenNest.CNC; using OpenNest.Geometry; using OpenNest.Math; namespace OpenNest.Posts.Cincinnati; /// /// Shared utilities for splitting CNC programs into features and classifying them. /// public static class FeatureUtils { /// /// Splits a flat list of codes into feature groups, breaking on rapid moves. /// Each feature starts with a rapid move followed by cutting/etching moves. /// public static List> SplitByRapids(List codes) { var features = new List>(); List current = null; foreach (var code in codes) { if (code is RapidMove) { if (current != null) features.Add(current); current = new List { code }; } else { current ??= new List(); current.Add(code); } } if (current != null && current.Count > 0) features.Add(current); return features; } /// /// Classifies features as etch or cut and orders etch features before cut features. /// public static List<(List codes, bool isEtch)> ClassifyAndOrder(List> features) { var result = new List<(List, bool)>(); var etch = new List>(); var cut = new List>(); foreach (var f in features) { if (IsEtch(f)) etch.Add(f); else cut.Add(f); } foreach (var f in etch) result.Add((f, true)); foreach (var f in cut) result.Add((f, false)); return result; } /// /// Splits a part's program into features by rapids, classifies each as etch or cut, /// and orders etch features before cut features. /// public static List<(List codes, bool isEtch)> SplitAndClassify(Part part) => ClassifyAndOrder(SplitByRapids(part.Program.Codes)); /// /// Returns true if any non-rapid move in the feature has LayerType.Scribe. /// public static bool IsEtch(List codes) { foreach (var code in codes) { if (code is LinearMove linear && linear.Layer == LayerType.Scribe) return true; if (code is ArcMove arc && arc.Layer == LayerType.Scribe) return true; } return false; } /// /// Computes the total cut distance of a feature by summing segment lengths. /// public static double ComputeCutDistance(List codes) { var distance = 0.0; var currentPos = Vector.Zero; foreach (var code in codes) { if (code is RapidMove rapid) currentPos = rapid.EndPoint; else if (code is LinearMove linear) { distance += currentPos.DistanceTo(linear.EndPoint); currentPos = linear.EndPoint; } else if (code is ArcMove arc) { distance += ComputeArcLength(currentPos, arc); currentPos = arc.EndPoint; } } return distance; } /// /// Computes the arc length from the current position through an arc move. /// Uses radius * sweep angle instead of chord length. /// public static double ComputeArcLength(Vector startPos, ArcMove arc) { var radius = startPos.DistanceTo(arc.CenterPoint); if (radius < Tolerance.Epsilon) return 0.0; // Full circle: start ≈ end if (Tolerance.IsEqualTo(startPos.X, arc.EndPoint.X) && Tolerance.IsEqualTo(startPos.Y, arc.EndPoint.Y)) return 2.0 * System.Math.PI * radius; var startAngle = System.Math.Atan2( startPos.Y - arc.CenterPoint.Y, startPos.X - arc.CenterPoint.X); var endAngle = System.Math.Atan2( arc.EndPoint.Y - arc.CenterPoint.Y, arc.EndPoint.X - arc.CenterPoint.X); double sweep; if (arc.Rotation == RotationType.CW) { sweep = startAngle - endAngle; if (sweep <= 0) sweep += 2.0 * System.Math.PI; } else { sweep = endAngle - startAngle; if (sweep <= 0) sweep += 2.0 * System.Math.PI; } return radius * sweep; } }