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