Every DefaultPlateFiller.Fill makes a fresh canonical copy of the drawing, and BestFitCache/FillResultCache keyed by drawing reference, so fills never shared results and the static caches grew without bound. CanonicalFrame now records which drawing each canonical copy came from. Both caches key weakly on that source drawing, so every canonical copy shares one entry and released drawings can be collected. An entry is dropped when the drawing's Program instance or canonical angle changes. Best-fit candidates are computed once per (drawing, spacing) through BestFitFinder.FindCandidates and filtered per plate size with the same filter FindBestFits uses. FillResultCache keeps canonical and non-canonical callers apart. Adds Debug-only PerfCounters for best-fit runs, offset perimeter builds and Part.Intersects calls. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
303 lines
11 KiB
C#
303 lines
11 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();
|
|
_filter = CreateFilter(maxPlateWidth, maxPlateHeight);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The filter <see cref="FindBestFits"/> applies for a plate of the given size. The
|
|
/// aspect-ratio limit widens with the plate's own aspect.
|
|
/// </summary>
|
|
public static BestFitFilter CreateFilter(double maxPlateWidth, double maxPlateHeight)
|
|
{
|
|
var plateAspect =
|
|
System.Math.Max(maxPlateWidth, maxPlateHeight)
|
|
/ System.Math.Max(System.Math.Min(maxPlateWidth, maxPlateHeight), 0.001);
|
|
return 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 results = Evaluate(drawing, spacing, stepSize);
|
|
|
|
_filter.Apply(results);
|
|
|
|
return Number(SortResults(results, sortBy));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Evaluates every pair candidate without the plate-size filter, sorted by area. Applying
|
|
/// <see cref="CreateFilter"/> for a plate size to shallow copies of these results gives
|
|
/// the same results <see cref="FindBestFits"/> returns for that size, so one run can
|
|
/// serve several sizes.
|
|
/// </summary>
|
|
public List<BestFitResult> FindCandidates(
|
|
Drawing drawing,
|
|
double spacing = 0.25,
|
|
double stepSize = 0.25
|
|
)
|
|
{
|
|
return Number(SortResults(Evaluate(drawing, spacing, stepSize), BestFitSortField.Area));
|
|
}
|
|
|
|
private static List<BestFitResult> Number(List<BestFitResult> results)
|
|
{
|
|
for (var i = 0; i < results.Count; i++)
|
|
results[i].Candidate.TestNumber = i;
|
|
|
|
return results;
|
|
}
|
|
|
|
private List<BestFitResult> Evaluate(Drawing drawing, double spacing, double stepSize)
|
|
{
|
|
PerfCounters.CountFindBestFits();
|
|
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"
|
|
);
|
|
|
|
return _evaluator.EvaluateAll(allCandidates);
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
}
|