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;
}
}