mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-14 06:55:07 -05:00
Geometry, nesting and import code now call PortableMath instead of System.Math for Sin, Cos, Tan, Atan, Atan2, Asin and Acos (Core, Engine, IO, Data; 269 calls in 64 files). A test fails if those projects call the runtime's trig again. With the runtime's trig, Windows and Linux disagreed in the last bit at non-quarter angles and the same job could nest differently: the learned-angle nests (a cross fixture) placed 23 parts in Strip on Windows and 28 on Linux. Now every Engine.Tests pin gives the same layout on both (3600 passing on Linux and on DesktopPC), Linux layouts are unchanged, and the learned-angle rows are re-pinned to the shared layouts, which pass the overlap and work-area checks, and run on every platform again. Fixes Project Memory 518f3b58.
276 lines
9.7 KiB
C#
276 lines
9.7 KiB
C#
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);
|
|
|
|
// Slots in strategy (angle) order, so candidates keep generation order however the
|
|
// workers finish: the stable area sort then breaks ties the same way on every run.
|
|
var candidateLists = new List<PairCandidate>[strategies.Count];
|
|
|
|
Parallel.For(
|
|
0,
|
|
strategies.Count,
|
|
i =>
|
|
{
|
|
candidateLists[i] = strategies[i].GenerateCandidates(drawing, spacing, stepSize);
|
|
}
|
|
);
|
|
|
|
var allCandidates = candidateLists.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 => SpecialLayers.IsMaterial(e.Layer));
|
|
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());
|
|
SpatialQuery.AddArcExtremeVertices(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(PortableMath.Atan2(dy, dx));
|
|
AddUniqueAngle(hullAngles, angle);
|
|
}
|
|
|
|
return hullAngles;
|
|
}
|
|
|
|
/// <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;
|
|
}
|
|
}
|
|
}
|
|
}
|