feat: integrate GPU slide computation into best-fit pipeline
Thread ISlideComputer through BestFitCache → BestFitFinder → RotationSlideStrategy. RotationSlideStrategy now collects all offsets across 4 push directions and dispatches them in a single batch (GPU or CPU fallback). Also improves rotation angle extraction: uses raw geometry (line endpoints + arc cardinal extremes) instead of tessellation to avoid flooding the hull with near-duplicate edge angles, and adds a 5-degree deduplication threshold. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,7 @@ namespace OpenNest.Engine.BestFit
|
||||
new ConcurrentDictionary<CacheKey, List<BestFitResult>>();
|
||||
|
||||
public static Func<Drawing, double, IPairEvaluator> CreateEvaluator { get; set; }
|
||||
public static Func<ISlideComputer> CreateSlideComputer { get; set; }
|
||||
|
||||
public static List<BestFitResult> GetOrCompute(
|
||||
Drawing drawing, double plateWidth, double plateHeight,
|
||||
@@ -24,6 +25,7 @@ namespace OpenNest.Engine.BestFit
|
||||
return cached;
|
||||
|
||||
IPairEvaluator evaluator = null;
|
||||
ISlideComputer slideComputer = null;
|
||||
|
||||
try
|
||||
{
|
||||
@@ -33,7 +35,13 @@ namespace OpenNest.Engine.BestFit
|
||||
catch { /* fall back to default evaluator */ }
|
||||
}
|
||||
|
||||
var finder = new BestFitFinder(plateWidth, plateHeight, evaluator);
|
||||
if (CreateSlideComputer != null)
|
||||
{
|
||||
try { slideComputer = CreateSlideComputer(); }
|
||||
catch { /* fall back to CPU slide computation */ }
|
||||
}
|
||||
|
||||
var finder = new BestFitFinder(plateWidth, plateHeight, evaluator, slideComputer);
|
||||
var results = finder.FindBestFits(drawing, spacing, StepSize);
|
||||
|
||||
_cache.TryAdd(key, results);
|
||||
@@ -42,6 +50,7 @@ namespace OpenNest.Engine.BestFit
|
||||
finally
|
||||
{
|
||||
(evaluator as IDisposable)?.Dispose();
|
||||
// Slide computer is managed by the factory as a singleton — don't dispose here
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,11 +12,14 @@ namespace OpenNest.Engine.BestFit
|
||||
public class BestFitFinder
|
||||
{
|
||||
private readonly IPairEvaluator _evaluator;
|
||||
private readonly ISlideComputer _slideComputer;
|
||||
private readonly BestFitFilter _filter;
|
||||
|
||||
public BestFitFinder(double maxPlateWidth, double maxPlateHeight, IPairEvaluator evaluator = null)
|
||||
public BestFitFinder(double maxPlateWidth, double maxPlateHeight,
|
||||
IPairEvaluator evaluator = null, ISlideComputer slideComputer = null)
|
||||
{
|
||||
_evaluator = evaluator ?? new PairEvaluator();
|
||||
_slideComputer = slideComputer;
|
||||
_filter = new BestFitFilter
|
||||
{
|
||||
MaxPlateWidth = maxPlateWidth,
|
||||
@@ -78,7 +81,7 @@ namespace OpenNest.Engine.BestFit
|
||||
foreach (var angle in angles)
|
||||
{
|
||||
var desc = string.Format("{0:F1} deg rotated, offset slide", Angle.ToDegrees(angle));
|
||||
strategies.Add(new RotationSlideStrategy(angle, type++, desc));
|
||||
strategies.Add(new RotationSlideStrategy(angle, type++, desc, _slideComputer));
|
||||
}
|
||||
|
||||
return strategies;
|
||||
@@ -102,6 +105,7 @@ namespace OpenNest.Engine.BestFit
|
||||
AddUniqueAngle(angles, Angle.NormalizeRad(hullAngle + System.Math.PI));
|
||||
}
|
||||
|
||||
angles.Sort();
|
||||
return angles;
|
||||
}
|
||||
|
||||
@@ -115,8 +119,24 @@ namespace OpenNest.Engine.BestFit
|
||||
|
||||
foreach (var shape in shapes)
|
||||
{
|
||||
var polygon = shape.ToPolygonWithTolerance(0.01);
|
||||
points.AddRange(polygon.Vertices);
|
||||
// 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)
|
||||
@@ -143,13 +163,49 @@ namespace OpenNest.Engine.BestFit
|
||||
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))
|
||||
if (existing.IsEqualTo(angle, AngleTolerance))
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,11 +5,20 @@ namespace OpenNest.Engine.BestFit
|
||||
{
|
||||
public class RotationSlideStrategy : IBestFitStrategy
|
||||
{
|
||||
public RotationSlideStrategy(double part2Rotation, int type, string description)
|
||||
private readonly ISlideComputer _slideComputer;
|
||||
|
||||
private static readonly PushDirection[] AllDirections =
|
||||
{
|
||||
PushDirection.Left, PushDirection.Down, PushDirection.Right, PushDirection.Up
|
||||
};
|
||||
|
||||
public RotationSlideStrategy(double part2Rotation, int type, string description,
|
||||
ISlideComputer slideComputer = null)
|
||||
{
|
||||
Part2Rotation = part2Rotation;
|
||||
Type = type;
|
||||
Description = description;
|
||||
_slideComputer = slideComputer;
|
||||
}
|
||||
|
||||
public double Part2Rotation { get; }
|
||||
@@ -23,43 +32,66 @@ namespace OpenNest.Engine.BestFit
|
||||
var part1 = Part.CreateAtOrigin(drawing);
|
||||
var part2Template = Part.CreateAtOrigin(drawing, Part2Rotation);
|
||||
|
||||
var halfSpacing = spacing / 2;
|
||||
var part1Lines = Helper.GetOffsetPartLines(part1, halfSpacing);
|
||||
var part2TemplateLines = Helper.GetOffsetPartLines(part2Template, halfSpacing);
|
||||
|
||||
var bbox1 = part1.BoundingBox;
|
||||
var bbox2 = part2Template.BoundingBox;
|
||||
|
||||
// Collect offsets and directions across all 4 axes
|
||||
var allDx = new List<double>();
|
||||
var allDy = new List<double>();
|
||||
var allDirs = new List<PushDirection>();
|
||||
|
||||
foreach (var pushDir in AllDirections)
|
||||
BuildOffsets(bbox1, bbox2, spacing, stepSize, pushDir, allDx, allDy, allDirs);
|
||||
|
||||
if (allDx.Count == 0)
|
||||
return candidates;
|
||||
|
||||
// Compute all distances — single GPU dispatch or CPU loop
|
||||
var distances = ComputeAllDistances(
|
||||
part1Lines, part2TemplateLines, allDx, allDy, allDirs);
|
||||
|
||||
// Create candidates from valid results
|
||||
var testNumber = 0;
|
||||
|
||||
// Try pushing left (horizontal slide)
|
||||
GenerateCandidatesForAxis(
|
||||
part1, part2Template, drawing, spacing, stepSize,
|
||||
PushDirection.Left, candidates, ref testNumber);
|
||||
for (var i = 0; i < allDx.Count; i++)
|
||||
{
|
||||
var slideDist = distances[i];
|
||||
if (slideDist >= double.MaxValue || slideDist < 0)
|
||||
continue;
|
||||
|
||||
// Try pushing down (vertical slide)
|
||||
GenerateCandidatesForAxis(
|
||||
part1, part2Template, drawing, spacing, stepSize,
|
||||
PushDirection.Down, candidates, ref testNumber);
|
||||
var dx = allDx[i];
|
||||
var dy = allDy[i];
|
||||
var pushVector = GetPushVector(allDirs[i], slideDist);
|
||||
var finalPosition = new Vector(
|
||||
part2Template.Location.X + dx + pushVector.X,
|
||||
part2Template.Location.Y + dy + pushVector.Y);
|
||||
|
||||
// Try pushing right (approach from left — finds concave interlocking)
|
||||
GenerateCandidatesForAxis(
|
||||
part1, part2Template, drawing, spacing, stepSize,
|
||||
PushDirection.Right, candidates, ref testNumber);
|
||||
|
||||
// Try pushing up (approach from below — finds concave interlocking)
|
||||
GenerateCandidatesForAxis(
|
||||
part1, part2Template, drawing, spacing, stepSize,
|
||||
PushDirection.Up, candidates, ref testNumber);
|
||||
candidates.Add(new PairCandidate
|
||||
{
|
||||
Drawing = drawing,
|
||||
Part1Rotation = 0,
|
||||
Part2Rotation = Part2Rotation,
|
||||
Part2Offset = finalPosition,
|
||||
StrategyType = Type,
|
||||
TestNumber = testNumber++,
|
||||
Spacing = spacing
|
||||
});
|
||||
}
|
||||
|
||||
return candidates;
|
||||
}
|
||||
|
||||
private void GenerateCandidatesForAxis(
|
||||
Part part1, Part part2Template, Drawing drawing,
|
||||
double spacing, double stepSize, PushDirection pushDir,
|
||||
List<PairCandidate> candidates, ref int testNumber)
|
||||
private static void BuildOffsets(
|
||||
Box bbox1, Box bbox2, double spacing, double stepSize,
|
||||
PushDirection pushDir, List<double> allDx, List<double> allDy,
|
||||
List<PushDirection> allDirs)
|
||||
{
|
||||
var bbox1 = part1.BoundingBox;
|
||||
var bbox2 = part2Template.BoundingBox;
|
||||
var halfSpacing = spacing / 2;
|
||||
|
||||
var isHorizontalPush = pushDir == PushDirection.Left || pushDir == PushDirection.Right;
|
||||
|
||||
// Perpendicular range: part2 slides across the full extent of part1
|
||||
double perpMin, perpMax, pushStartOffset;
|
||||
|
||||
if (isHorizontalPush)
|
||||
@@ -75,54 +107,55 @@ namespace OpenNest.Engine.BestFit
|
||||
pushStartOffset = bbox1.Length + bbox2.Length + spacing * 2;
|
||||
}
|
||||
|
||||
// Pre-compute part1's offset lines (half-spacing outward)
|
||||
var part1Lines = Helper.GetOffsetPartLines(part1, halfSpacing);
|
||||
|
||||
// Align sweep start to a multiple of stepSize so that offset=0 is always
|
||||
// included. This ensures perfect grid arrangements (side-by-side, stacked)
|
||||
// are generated for rectangular parts.
|
||||
var alignedStart = System.Math.Ceiling(perpMin / stepSize) * stepSize;
|
||||
|
||||
for (var offset = alignedStart; offset <= perpMax; offset += stepSize)
|
||||
{
|
||||
var part2 = (Part)part2Template.Clone();
|
||||
|
||||
// Place part2 far away along push axis, at perpendicular offset.
|
||||
// Left/Down: start on the positive side; Right/Up: start on the negative side.
|
||||
var isPositiveStart = pushDir == PushDirection.Left || pushDir == PushDirection.Down;
|
||||
var startPos = isPositiveStart ? pushStartOffset : -pushStartOffset;
|
||||
|
||||
if (isHorizontalPush)
|
||||
part2.Offset(startPos, offset);
|
||||
else
|
||||
part2.Offset(offset, startPos);
|
||||
|
||||
// Get part2's offset lines (half-spacing outward)
|
||||
var part2Lines = Helper.GetOffsetPartLines(part2, halfSpacing);
|
||||
|
||||
// Find contact distance
|
||||
var slideDist = Helper.DirectionalDistance(part2Lines, part1Lines, pushDir);
|
||||
|
||||
if (slideDist >= double.MaxValue || slideDist < 0)
|
||||
continue;
|
||||
|
||||
// Move part2 to contact position
|
||||
var pushVector = GetPushVector(pushDir, slideDist);
|
||||
var finalPosition = part2.Location + pushVector;
|
||||
|
||||
candidates.Add(new PairCandidate
|
||||
for (var offset = alignedStart; offset <= perpMax; offset += stepSize)
|
||||
{
|
||||
Drawing = drawing,
|
||||
Part1Rotation = 0,
|
||||
Part2Rotation = Part2Rotation,
|
||||
Part2Offset = finalPosition,
|
||||
StrategyType = Type,
|
||||
TestNumber = testNumber++,
|
||||
Spacing = spacing
|
||||
});
|
||||
allDx.Add(isHorizontalPush ? startPos : offset);
|
||||
allDy.Add(isHorizontalPush ? offset : startPos);
|
||||
allDirs.Add(pushDir);
|
||||
}
|
||||
}
|
||||
|
||||
private double[] ComputeAllDistances(
|
||||
List<Line> part1Lines, List<Line> part2TemplateLines,
|
||||
List<double> allDx, List<double> allDy, List<PushDirection> allDirs)
|
||||
{
|
||||
var count = allDx.Count;
|
||||
|
||||
if (_slideComputer != null)
|
||||
{
|
||||
var stationarySegments = Helper.FlattenLines(part1Lines);
|
||||
var movingSegments = Helper.FlattenLines(part2TemplateLines);
|
||||
var offsets = new double[count * 2];
|
||||
var directions = new int[count];
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
offsets[i * 2] = allDx[i];
|
||||
offsets[i * 2 + 1] = allDy[i];
|
||||
directions[i] = (int)allDirs[i];
|
||||
}
|
||||
|
||||
return _slideComputer.ComputeBatchMultiDir(
|
||||
stationarySegments, part1Lines.Count,
|
||||
movingSegments, part2TemplateLines.Count,
|
||||
offsets, count, directions);
|
||||
}
|
||||
|
||||
var results = new double[count];
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
results[i] = Helper.DirectionalDistance(
|
||||
part2TemplateLines, allDx[i], allDy[i], part1Lines, allDirs[i]);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private static Vector GetPushVector(PushDirection direction, double distance)
|
||||
{
|
||||
switch (direction)
|
||||
|
||||
@@ -50,6 +50,9 @@ namespace OpenNest.Forms
|
||||
|
||||
//if (GpuEvaluatorFactory.GpuAvailable)
|
||||
// BestFitCache.CreateEvaluator = (drawing, spacing) => GpuEvaluatorFactory.Create(drawing, spacing);
|
||||
|
||||
if (GpuEvaluatorFactory.GpuAvailable)
|
||||
BestFitCache.CreateSlideComputer = () => GpuEvaluatorFactory.CreateSlideComputer();
|
||||
}
|
||||
|
||||
private Nest CreateDefaultNest()
|
||||
|
||||
Reference in New Issue
Block a user