refactor(engine): extract AngleCandidateBuilder from DefaultNestEngine
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
97
OpenNest.Engine/AngleCandidateBuilder.cs
Normal file
97
OpenNest.Engine/AngleCandidateBuilder.cs
Normal file
@@ -0,0 +1,97 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using OpenNest.Engine.ML;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class AngleCandidateBuilder
|
||||
{
|
||||
private readonly HashSet<double> knownGoodAngles = new();
|
||||
|
||||
public bool ForceFullSweep { get; set; }
|
||||
|
||||
public List<double> Build(NestItem item, double bestRotation, Box workArea)
|
||||
{
|
||||
var angles = new List<double> { bestRotation, bestRotation + Angle.HalfPI };
|
||||
|
||||
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);
|
||||
var needsSweep = workAreaShortSide < partLongestSide || ForceFullSweep;
|
||||
|
||||
if (needsSweep)
|
||||
{
|
||||
var step = Angle.ToRadians(5);
|
||||
for (var a = 0.0; a < System.Math.PI; a += step)
|
||||
{
|
||||
if (!angles.Any(existing => existing.IsEqualTo(a)))
|
||||
angles.Add(a);
|
||||
}
|
||||
}
|
||||
|
||||
if (!ForceFullSweep && angles.Count > 2)
|
||||
{
|
||||
var features = FeatureExtractor.Extract(item.Drawing);
|
||||
if (features != null)
|
||||
{
|
||||
var predicted = AnglePredictor.PredictAngles(
|
||||
features, workArea.Width, workArea.Length);
|
||||
|
||||
if (predicted != null)
|
||||
{
|
||||
var mlAngles = new List<double>(predicted);
|
||||
|
||||
if (!mlAngles.Any(a => a.IsEqualTo(bestRotation)))
|
||||
mlAngles.Add(bestRotation);
|
||||
if (!mlAngles.Any(a => a.IsEqualTo(bestRotation + Angle.HalfPI)))
|
||||
mlAngles.Add(bestRotation + Angle.HalfPI);
|
||||
|
||||
Debug.WriteLine($"[AngleCandidateBuilder] ML: {angles.Count} angles -> {mlAngles.Count} predicted");
|
||||
angles = mlAngles;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (knownGoodAngles.Count > 0 && !ForceFullSweep)
|
||||
{
|
||||
var pruned = new List<double> { bestRotation, bestRotation + Angle.HalfPI };
|
||||
|
||||
foreach (var a in knownGoodAngles)
|
||||
{
|
||||
if (!pruned.Any(existing => existing.IsEqualTo(a)))
|
||||
pruned.Add(a);
|
||||
}
|
||||
|
||||
Debug.WriteLine($"[AngleCandidateBuilder] Pruned: {angles.Count} -> {pruned.Count} angles (known-good)");
|
||||
return pruned;
|
||||
}
|
||||
|
||||
return angles;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records angles that produced results. These are used to prune
|
||||
/// subsequent Build() calls.
|
||||
/// </summary>
|
||||
public void RecordProductive(List<AngleResult> angleResults)
|
||||
{
|
||||
foreach (var ar in angleResults)
|
||||
{
|
||||
if (ar.PartCount > 0)
|
||||
knownGoodAngles.Add(Angle.ToRadians(ar.AngleDeg));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
83
OpenNest.Tests/AngleCandidateBuilderTests.cs
Normal file
83
OpenNest.Tests/AngleCandidateBuilderTests.cs
Normal file
@@ -0,0 +1,83 @@
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Tests;
|
||||
|
||||
public class AngleCandidateBuilderTests
|
||||
{
|
||||
private static Drawing MakeRectDrawing(double w, double h)
|
||||
{
|
||||
var pgm = new OpenNest.CNC.Program();
|
||||
pgm.Codes.Add(new OpenNest.CNC.RapidMove(new Vector(0, 0)));
|
||||
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(w, 0)));
|
||||
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(w, h)));
|
||||
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(0, h)));
|
||||
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(0, 0)));
|
||||
return new Drawing("rect", pgm);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_ReturnsAtLeastTwoAngles()
|
||||
{
|
||||
var builder = new AngleCandidateBuilder();
|
||||
var item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
|
||||
var workArea = new Box(0, 0, 100, 100);
|
||||
|
||||
var angles = builder.Build(item, 0, workArea);
|
||||
|
||||
Assert.True(angles.Count >= 2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_NarrowWorkArea_ProducesMoreAngles()
|
||||
{
|
||||
var builder = new AngleCandidateBuilder();
|
||||
var item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
|
||||
var wideArea = new Box(0, 0, 100, 100);
|
||||
var narrowArea = new Box(0, 0, 100, 8); // narrower than part's longest side
|
||||
|
||||
var wideAngles = builder.Build(item, 0, wideArea);
|
||||
var narrowAngles = builder.Build(item, 0, narrowArea);
|
||||
|
||||
Assert.True(narrowAngles.Count > wideAngles.Count,
|
||||
$"Narrow ({narrowAngles.Count}) should have more angles than wide ({wideAngles.Count})");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ForceFullSweep_ProducesFullSweep()
|
||||
{
|
||||
var builder = new AngleCandidateBuilder { ForceFullSweep = true };
|
||||
var item = new NestItem { Drawing = MakeRectDrawing(5, 5) };
|
||||
var workArea = new Box(0, 0, 100, 100);
|
||||
|
||||
var angles = builder.Build(item, 0, workArea);
|
||||
|
||||
// Full sweep at 5deg steps = ~36 angles (0 to 175), plus base angles
|
||||
Assert.True(angles.Count > 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RecordProductive_PrunesSubsequentBuilds()
|
||||
{
|
||||
var builder = new AngleCandidateBuilder { ForceFullSweep = true };
|
||||
var item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
|
||||
var workArea = new Box(0, 0, 100, 8);
|
||||
|
||||
// First build — full sweep
|
||||
var firstAngles = builder.Build(item, 0, workArea);
|
||||
|
||||
// Record some as productive
|
||||
var productive = new List<AngleResult>
|
||||
{
|
||||
new AngleResult { AngleDeg = 0, PartCount = 5 },
|
||||
new AngleResult { AngleDeg = 45, PartCount = 3 },
|
||||
};
|
||||
builder.RecordProductive(productive);
|
||||
|
||||
// Second build — should be pruned to known-good + base angles
|
||||
builder.ForceFullSweep = false;
|
||||
var secondAngles = builder.Build(item, 0, workArea);
|
||||
|
||||
Assert.True(secondAngles.Count < firstAngles.Count,
|
||||
$"Pruned ({secondAngles.Count}) should be fewer than full ({firstAngles.Count})");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user