Files
OpenNest/OpenNest.Engine/BestFit/BestFitFinder.cs
T
aj aec0523062 style: apply CSharpier formatting to all C# sources
Repo-wide sweep with the pinned CSharpier 1.3.0 tool. Whitespace and
line-wrapping only; OpenNest.Engine.Tests (109) and OpenNest.IO.Tests
pass after reformat, full solution builds 0 errors.

Added .csharpierignore so csproj/config XML keeps its existing layout
(CSharpier's XML wrapping churns attributes with zero benefit).

Formatting is now enforceable: dotnet csharpier check . passes.
2026-09-20 16:41:50 -04:00

270 lines
9.3 KiB
C#

using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using OpenNest.Converters;
using OpenNest.Engine.BestFit.Tiling;
using OpenNest.Geometry;
using OpenNest.Math;
namespace OpenNest.Engine.BestFit
{
public class BestFitFinder
{
private readonly IPairEvaluator _evaluator;
private readonly IDistanceComputer _distanceComputer;
private readonly BestFitFilter _filter;
public BestFitFinder(
double maxPlateWidth,
double maxPlateHeight,
IPairEvaluator evaluator = null,
ISlideComputer slideComputer = null
)
{
_evaluator = evaluator ?? new PairEvaluator();
_distanceComputer =
slideComputer != null
? (IDistanceComputer)new GpuDistanceComputer(slideComputer)
: new CpuDistanceComputer();
var plateAspect =
System.Math.Max(maxPlateWidth, maxPlateHeight)
/ System.Math.Max(System.Math.Min(maxPlateWidth, maxPlateHeight), 0.001);
_filter = new BestFitFilter
{
MaxPlateWidth = maxPlateWidth,
MaxPlateHeight = maxPlateHeight,
MaxAspectRatio = System.Math.Max(5.0, plateAspect),
};
}
public List<BestFitResult> FindBestFits(
Drawing drawing,
double spacing = 0.25,
double stepSize = 0.25,
BestFitSortField sortBy = BestFitSortField.Area
)
{
var strategies = BuildStrategies(drawing, spacing);
var candidateBags = new ConcurrentBag<List<PairCandidate>>();
Parallel.ForEach(
strategies,
strategy =>
{
candidateBags.Add(strategy.GenerateCandidates(drawing, spacing, stepSize));
}
);
var allCandidates = candidateBags.SelectMany(c => c).ToList();
Debug.WriteLine(
$"[BestFitFinder] {strategies.Count} strategies, {allCandidates.Count} candidates"
);
var results = _evaluator.EvaluateAll(allCandidates);
_filter.Apply(results);
results = SortResults(results, sortBy);
for (var i = 0; i < results.Count; i++)
results[i].Candidate.TestNumber = i;
return results;
}
public List<TileResult> FindAndTile(
Drawing drawing,
Plate plate,
double spacing = 0.25,
double stepSize = 0.25,
int topN = 10
)
{
var bestFits = FindBestFits(drawing, spacing, stepSize);
var tileEvaluator = new TileEvaluator();
return bestFits
.Where(r => r.Keep)
.Take(topN)
.Select(r => tileEvaluator.Evaluate(r, plate))
.OrderByDescending(t => t.PartsNested)
.ThenByDescending(t => t.Utilization)
.ToList();
}
private List<IBestFitStrategy> BuildStrategies(Drawing drawing, double spacing)
{
var angles = GetRotationAngles(drawing);
var strategies = new List<IBestFitStrategy>();
var index = 1;
foreach (var angle in angles)
{
var desc = string.Format(
"{0:F1} deg rotated, offset slide",
Angle.ToDegrees(angle)
);
strategies.Add(new RotationSlideStrategy(angle, index++, desc, _distanceComputer));
}
return strategies;
}
private List<double> GetRotationAngles(Drawing drawing)
{
var angles = new List<double> { 0, Angle.HalfPI, System.Math.PI, Angle.HalfPI * 3 };
var hullAngles = GetHullEdgeAngles(drawing);
foreach (var hullAngle in hullAngles)
{
AddUniqueAngle(angles, hullAngle);
AddUniqueAngle(angles, Angle.NormalizeRad(hullAngle + System.Math.PI));
}
angles.Sort();
return angles;
}
private List<double> GetHullEdgeAngles(Drawing drawing)
{
var entities = ConvertProgram
.ToGeometry(drawing.Program)
.Where(e => e.Layer != SpecialLayers.Rapid);
var shapes = ShapeBuilder.GetShapes(entities);
var points = new List<Vector>();
foreach (var shape in shapes)
{
// Extract key points from original geometry — line endpoints
// plus arc endpoints and cardinal extreme points. This avoids
// tessellating arcs into many chords that flood the hull with
// near-duplicate edge angles.
foreach (var entity in shape.Entities)
{
if (entity is Line line)
{
points.Add(line.StartPoint);
points.Add(line.EndPoint);
}
else if (entity is Arc arc)
{
points.Add(arc.StartPoint());
points.Add(arc.EndPoint());
AddArcExtremes(points, arc);
}
}
}
if (points.Count < 3)
return new List<double>();
var hull = ConvexHull.Compute(points);
var vertices = hull.Vertices;
var n = hull.IsClosed() ? vertices.Count - 1 : vertices.Count;
var hullAngles = new List<double>();
for (var i = 0; i < n; i++)
{
var next = (i + 1) % n;
var dx = vertices[next].X - vertices[i].X;
var dy = vertices[next].Y - vertices[i].Y;
if (dx * dx + dy * dy < Tolerance.Epsilon)
continue;
var angle = Angle.NormalizeRad(System.Math.Atan2(dy, dx));
AddUniqueAngle(hullAngles, angle);
}
return hullAngles;
}
/// <summary>
/// Adds the cardinal extreme points of an arc (0°, 90°, 180°, 270°)
/// if they fall within the arc's angular span.
/// </summary>
private static void AddArcExtremes(List<Vector> points, Arc arc)
{
var a1 = arc.StartAngle;
var a2 = arc.EndAngle;
if (arc.IsReversed)
Generic.Swap(ref a1, ref a2);
// Right (0°)
if (Angle.IsBetweenRad(Angle.TwoPI, a1, a2))
points.Add(new Vector(arc.Center.X + arc.Radius, arc.Center.Y));
// Top (90°)
if (Angle.IsBetweenRad(Angle.HalfPI, a1, a2))
points.Add(new Vector(arc.Center.X, arc.Center.Y + arc.Radius));
// Left (180°)
if (Angle.IsBetweenRad(System.Math.PI, a1, a2))
points.Add(new Vector(arc.Center.X - arc.Radius, arc.Center.Y));
// Bottom (270°)
if (Angle.IsBetweenRad(System.Math.PI * 1.5, a1, a2))
points.Add(new Vector(arc.Center.X, arc.Center.Y - arc.Radius));
}
/// <summary>
/// Minimum angular separation (radians) between hull-derived rotation candidates.
/// Tessellated arcs produce many hull edges with nearly identical angles;
/// a 1° threshold collapses those into a single representative.
/// </summary>
private const double AngleTolerance = System.Math.PI / 36; // 5 degrees
private static void AddUniqueAngle(List<double> angles, double angle)
{
angle = Angle.NormalizeRad(angle);
foreach (var existing in angles)
{
if (existing.IsEqualTo(angle, AngleTolerance))
return;
}
angles.Add(angle);
}
private List<BestFitResult> SortResults(
List<BestFitResult> results,
BestFitSortField sortBy
)
{
switch (sortBy)
{
case BestFitSortField.Area:
return results.OrderBy(r => r.RotatedArea).ToList();
case BestFitSortField.LongestSide:
return results.OrderBy(r => r.LongestSide).ToList();
case BestFitSortField.ShortestSide:
return results.OrderBy(r => r.ShortestSide).ToList();
case BestFitSortField.Type:
return results
.OrderBy(r => r.Candidate.StrategyIndex)
.ThenBy(r => r.Candidate.TestNumber)
.ToList();
case BestFitSortField.OriginalSequence:
return results.OrderBy(r => r.Candidate.TestNumber).ToList();
case BestFitSortField.Keep:
return results
.OrderByDescending(r => r.Keep)
.ThenBy(r => r.RotatedArea)
.ToList();
case BestFitSortField.WhyKeepDrop:
return results.OrderBy(r => r.Reason).ThenBy(r => r.RotatedArea).ToList();
default:
return results;
}
}
}
}