using OpenNest.Engine.ML;
using OpenNest.Geometry;
using OpenNest.Math;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
namespace OpenNest.Engine.Fill
{
///
/// Builds candidate rotation angles for single-item fill. Encapsulates the
/// full pipeline: base angles, narrow-area sweep, ML prediction, and
/// known-good pruning across fills.
///
public class AngleCandidateBuilder
{
private readonly HashSet knownGoodAngles = new();
public bool ForceFullSweep { get; set; }
public List Build(NestItem item, double bestRotation, Box workArea)
{
var baseAngles = new[] { bestRotation, bestRotation + Angle.HalfPI };
if (knownGoodAngles.Count > 0 && !ForceFullSweep)
return BuildPrunedList(baseAngles);
var angles = new List(baseAngles);
if (NeedsSweep(item, bestRotation, workArea))
AddSweepAngles(angles);
if (!ForceFullSweep && angles.Count > 2)
angles = ApplyMlPrediction(item, workArea, baseAngles, angles);
return angles;
}
private bool NeedsSweep(NestItem item, double bestRotation, Box workArea)
{
var testPart = new Part(item.Drawing);
if (!bestRotation.IsEqualTo(0))
testPart.Rotate(bestRotation);
testPart.UpdateBounds();
var partLongestSide = System.Math.Max(testPart.BoundingBox.Width, testPart.BoundingBox.Length);
var workAreaShortSide = System.Math.Min(workArea.Width, workArea.Length);
return workAreaShortSide < partLongestSide || ForceFullSweep;
}
private static void AddSweepAngles(List angles)
{
var step = Angle.ToRadians(5);
for (var a = 0.0; a < System.Math.PI; a += step)
{
if (!ContainsAngle(angles, a))
angles.Add(a);
}
}
private static List ApplyMlPrediction(
NestItem item, Box workArea, double[] baseAngles, List fallback)
{
var features = FeatureExtractor.Extract(item.Drawing);
if (features == null)
return fallback;
var predicted = AnglePredictor.PredictAngles(features, workArea.Width, workArea.Length);
if (predicted == null)
return fallback;
var mlAngles = new List(predicted);
foreach (var b in baseAngles)
{
if (!ContainsAngle(mlAngles, b))
mlAngles.Add(b);
}
Debug.WriteLine($"[AngleCandidateBuilder] ML: {fallback.Count} angles -> {mlAngles.Count} predicted");
return mlAngles;
}
private List BuildPrunedList(double[] baseAngles)
{
var pruned = new List(baseAngles);
foreach (var a in knownGoodAngles)
{
if (!ContainsAngle(pruned, a))
pruned.Add(a);
}
Debug.WriteLine($"[AngleCandidateBuilder] Pruned to {pruned.Count} angles (known-good)");
return pruned;
}
private static bool ContainsAngle(List angles, double angle)
{
return angles.Any(existing => existing.IsEqualTo(angle));
}
///
/// Records angles that produced results. These are used to prune
/// subsequent Build() calls.
///
public void RecordProductive(List angleResults)
{
foreach (var ar in angleResults)
{
if (ar.PartCount > 0)
knownGoodAngles.Add(Angle.ToRadians(ar.AngleDeg));
}
}
}
}