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>>();
|
new ConcurrentDictionary<CacheKey, List<BestFitResult>>();
|
||||||
|
|
||||||
public static Func<Drawing, double, IPairEvaluator> CreateEvaluator { get; set; }
|
public static Func<Drawing, double, IPairEvaluator> CreateEvaluator { get; set; }
|
||||||
|
public static Func<ISlideComputer> CreateSlideComputer { get; set; }
|
||||||
|
|
||||||
public static List<BestFitResult> GetOrCompute(
|
public static List<BestFitResult> GetOrCompute(
|
||||||
Drawing drawing, double plateWidth, double plateHeight,
|
Drawing drawing, double plateWidth, double plateHeight,
|
||||||
@@ -24,6 +25,7 @@ namespace OpenNest.Engine.BestFit
|
|||||||
return cached;
|
return cached;
|
||||||
|
|
||||||
IPairEvaluator evaluator = null;
|
IPairEvaluator evaluator = null;
|
||||||
|
ISlideComputer slideComputer = null;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -33,7 +35,13 @@ namespace OpenNest.Engine.BestFit
|
|||||||
catch { /* fall back to default evaluator */ }
|
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);
|
var results = finder.FindBestFits(drawing, spacing, StepSize);
|
||||||
|
|
||||||
_cache.TryAdd(key, results);
|
_cache.TryAdd(key, results);
|
||||||
@@ -42,6 +50,7 @@ namespace OpenNest.Engine.BestFit
|
|||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
(evaluator as IDisposable)?.Dispose();
|
(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
|
public class BestFitFinder
|
||||||
{
|
{
|
||||||
private readonly IPairEvaluator _evaluator;
|
private readonly IPairEvaluator _evaluator;
|
||||||
|
private readonly ISlideComputer _slideComputer;
|
||||||
private readonly BestFitFilter _filter;
|
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();
|
_evaluator = evaluator ?? new PairEvaluator();
|
||||||
|
_slideComputer = slideComputer;
|
||||||
_filter = new BestFitFilter
|
_filter = new BestFitFilter
|
||||||
{
|
{
|
||||||
MaxPlateWidth = maxPlateWidth,
|
MaxPlateWidth = maxPlateWidth,
|
||||||
@@ -78,7 +81,7 @@ namespace OpenNest.Engine.BestFit
|
|||||||
foreach (var angle in angles)
|
foreach (var angle in angles)
|
||||||
{
|
{
|
||||||
var desc = string.Format("{0:F1} deg rotated, offset slide", Angle.ToDegrees(angle));
|
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;
|
return strategies;
|
||||||
@@ -102,6 +105,7 @@ namespace OpenNest.Engine.BestFit
|
|||||||
AddUniqueAngle(angles, Angle.NormalizeRad(hullAngle + System.Math.PI));
|
AddUniqueAngle(angles, Angle.NormalizeRad(hullAngle + System.Math.PI));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
angles.Sort();
|
||||||
return angles;
|
return angles;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,8 +119,24 @@ namespace OpenNest.Engine.BestFit
|
|||||||
|
|
||||||
foreach (var shape in shapes)
|
foreach (var shape in shapes)
|
||||||
{
|
{
|
||||||
var polygon = shape.ToPolygonWithTolerance(0.01);
|
// Extract key points from original geometry — line endpoints
|
||||||
points.AddRange(polygon.Vertices);
|
// 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)
|
if (points.Count < 3)
|
||||||
@@ -143,13 +163,49 @@ namespace OpenNest.Engine.BestFit
|
|||||||
return hullAngles;
|
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)
|
private static void AddUniqueAngle(List<double> angles, double angle)
|
||||||
{
|
{
|
||||||
angle = Angle.NormalizeRad(angle);
|
angle = Angle.NormalizeRad(angle);
|
||||||
|
|
||||||
foreach (var existing in angles)
|
foreach (var existing in angles)
|
||||||
{
|
{
|
||||||
if (existing.IsEqualTo(angle))
|
if (existing.IsEqualTo(angle, AngleTolerance))
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,11 +5,20 @@ namespace OpenNest.Engine.BestFit
|
|||||||
{
|
{
|
||||||
public class RotationSlideStrategy : IBestFitStrategy
|
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;
|
Part2Rotation = part2Rotation;
|
||||||
Type = type;
|
Type = type;
|
||||||
Description = description;
|
Description = description;
|
||||||
|
_slideComputer = slideComputer;
|
||||||
}
|
}
|
||||||
|
|
||||||
public double Part2Rotation { get; }
|
public double Part2Rotation { get; }
|
||||||
@@ -23,43 +32,66 @@ namespace OpenNest.Engine.BestFit
|
|||||||
var part1 = Part.CreateAtOrigin(drawing);
|
var part1 = Part.CreateAtOrigin(drawing);
|
||||||
var part2Template = Part.CreateAtOrigin(drawing, Part2Rotation);
|
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;
|
var testNumber = 0;
|
||||||
|
|
||||||
// Try pushing left (horizontal slide)
|
for (var i = 0; i < allDx.Count; i++)
|
||||||
GenerateCandidatesForAxis(
|
{
|
||||||
part1, part2Template, drawing, spacing, stepSize,
|
var slideDist = distances[i];
|
||||||
PushDirection.Left, candidates, ref testNumber);
|
if (slideDist >= double.MaxValue || slideDist < 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
// Try pushing down (vertical slide)
|
var dx = allDx[i];
|
||||||
GenerateCandidatesForAxis(
|
var dy = allDy[i];
|
||||||
part1, part2Template, drawing, spacing, stepSize,
|
var pushVector = GetPushVector(allDirs[i], slideDist);
|
||||||
PushDirection.Down, candidates, ref testNumber);
|
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)
|
candidates.Add(new PairCandidate
|
||||||
GenerateCandidatesForAxis(
|
{
|
||||||
part1, part2Template, drawing, spacing, stepSize,
|
Drawing = drawing,
|
||||||
PushDirection.Right, candidates, ref testNumber);
|
Part1Rotation = 0,
|
||||||
|
Part2Rotation = Part2Rotation,
|
||||||
// Try pushing up (approach from below — finds concave interlocking)
|
Part2Offset = finalPosition,
|
||||||
GenerateCandidatesForAxis(
|
StrategyType = Type,
|
||||||
part1, part2Template, drawing, spacing, stepSize,
|
TestNumber = testNumber++,
|
||||||
PushDirection.Up, candidates, ref testNumber);
|
Spacing = spacing
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return candidates;
|
return candidates;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void GenerateCandidatesForAxis(
|
private static void BuildOffsets(
|
||||||
Part part1, Part part2Template, Drawing drawing,
|
Box bbox1, Box bbox2, double spacing, double stepSize,
|
||||||
double spacing, double stepSize, PushDirection pushDir,
|
PushDirection pushDir, List<double> allDx, List<double> allDy,
|
||||||
List<PairCandidate> candidates, ref int testNumber)
|
List<PushDirection> allDirs)
|
||||||
{
|
{
|
||||||
var bbox1 = part1.BoundingBox;
|
|
||||||
var bbox2 = part2Template.BoundingBox;
|
|
||||||
var halfSpacing = spacing / 2;
|
|
||||||
|
|
||||||
var isHorizontalPush = pushDir == PushDirection.Left || pushDir == PushDirection.Right;
|
var isHorizontalPush = pushDir == PushDirection.Left || pushDir == PushDirection.Right;
|
||||||
|
|
||||||
// Perpendicular range: part2 slides across the full extent of part1
|
|
||||||
double perpMin, perpMax, pushStartOffset;
|
double perpMin, perpMax, pushStartOffset;
|
||||||
|
|
||||||
if (isHorizontalPush)
|
if (isHorizontalPush)
|
||||||
@@ -75,54 +107,55 @@ namespace OpenNest.Engine.BestFit
|
|||||||
pushStartOffset = bbox1.Length + bbox2.Length + spacing * 2;
|
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;
|
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 isPositiveStart = pushDir == PushDirection.Left || pushDir == PushDirection.Down;
|
||||||
var startPos = isPositiveStart ? pushStartOffset : -pushStartOffset;
|
var startPos = isPositiveStart ? pushStartOffset : -pushStartOffset;
|
||||||
|
|
||||||
if (isHorizontalPush)
|
for (var offset = alignedStart; offset <= perpMax; offset += stepSize)
|
||||||
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
|
|
||||||
{
|
{
|
||||||
Drawing = drawing,
|
allDx.Add(isHorizontalPush ? startPos : offset);
|
||||||
Part1Rotation = 0,
|
allDy.Add(isHorizontalPush ? offset : startPos);
|
||||||
Part2Rotation = Part2Rotation,
|
allDirs.Add(pushDir);
|
||||||
Part2Offset = finalPosition,
|
|
||||||
StrategyType = Type,
|
|
||||||
TestNumber = testNumber++,
|
|
||||||
Spacing = spacing
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
private static Vector GetPushVector(PushDirection direction, double distance)
|
||||||
{
|
{
|
||||||
switch (direction)
|
switch (direction)
|
||||||
|
|||||||
@@ -50,6 +50,9 @@ namespace OpenNest.Forms
|
|||||||
|
|
||||||
//if (GpuEvaluatorFactory.GpuAvailable)
|
//if (GpuEvaluatorFactory.GpuAvailable)
|
||||||
// BestFitCache.CreateEvaluator = (drawing, spacing) => GpuEvaluatorFactory.Create(drawing, spacing);
|
// BestFitCache.CreateEvaluator = (drawing, spacing) => GpuEvaluatorFactory.Create(drawing, spacing);
|
||||||
|
|
||||||
|
if (GpuEvaluatorFactory.GpuAvailable)
|
||||||
|
BestFitCache.CreateSlideComputer = () => GpuEvaluatorFactory.CreateSlideComputer();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Nest CreateDefaultNest()
|
private Nest CreateDefaultNest()
|
||||||
|
|||||||
Reference in New Issue
Block a user