From 5bebfcb6127f9ccac4cf96b208cf0fcaa07651da Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Sat, 7 Mar 2026 18:27:15 -0500 Subject: [PATCH] feat: wire GpuPairEvaluator into NestEngine with auto-detection NestEngine.CreateEvaluator factory delegate allows injection of GPU evaluator from UI layer. GpuEvaluatorFactory.Create attempts GPU, returns null (CPU fallback) if unavailable. All NestEngine call sites wired up. Co-Authored-By: Claude Opus 4.6 --- OpenNest.Engine/NestEngine.cs | 195 +++++++++++++++++++++++++++-- OpenNest/Actions/ActionClone.cs | 1 + OpenNest/Actions/ActionFillArea.cs | 6 +- OpenNest/Forms/MainForm.cs | 29 +++++ OpenNest/GpuEvaluatorFactory.cs | 22 ++++ OpenNest/OpenNest.csproj | 1 + 6 files changed, 237 insertions(+), 17 deletions(-) create mode 100644 OpenNest/GpuEvaluatorFactory.cs diff --git a/OpenNest.Engine/NestEngine.cs b/OpenNest.Engine/NestEngine.cs index 97a9c18..8befcf0 100644 --- a/OpenNest.Engine/NestEngine.cs +++ b/OpenNest.Engine/NestEngine.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using OpenNest.Converters; using OpenNest.Engine.BestFit; @@ -21,8 +22,12 @@ namespace OpenNest public NestDirection NestDirection { get; set; } + public Func CreateEvaluator { get; set; } + public bool Fill(NestItem item) { + var sw = Stopwatch.StartNew(); + var workArea = Plate.WorkArea(); var bestRotation = FindBestRotation(item); @@ -37,22 +42,27 @@ namespace OpenNest engine.Fill(item.Drawing, bestRotation + Angle.HalfPI, NestDirection.Vertical) }; - // Pick the linear configuration with the most parts. + // Pick the best valid linear configuration. List linearBest = null; foreach (var config in configs) { - if (linearBest == null || config.Count > linearBest.Count) + if (IsBetterValidFill(config, linearBest)) linearBest = config; } + var linearMs = sw.ElapsedMilliseconds; + // Try pair-based approach. var pairResult = FillWithPairs(item); - // Pick whichever produced more parts. + var pairMs = sw.ElapsedMilliseconds - linearMs; + + // Pick whichever is the better fill. + Debug.WriteLine($"[NestEngine.Fill] Linear: {linearBest?.Count ?? 0} parts ({linearMs}ms) | Pair: {pairResult.Count} parts ({pairMs}ms) | WorkArea: {workArea.Width:F1}x{workArea.Height:F1}"); var best = linearBest; - if (pairResult.Count > (best?.Count ?? 0)) + if (IsBetterFill(pairResult, best)) best = pairResult; if (best == null || best.Count == 0) @@ -76,6 +86,15 @@ namespace OpenNest var angles = FindHullEdgeAngles(groupParts); var best = FillPattern(engine, groupParts, angles); + // For single-part groups, also try pair-based filling. + if (groupParts.Count == 1) + { + var pairResult = FillWithPairs(new NestItem { Drawing = groupParts[0].BaseDrawing }); + + if (IsBetterFill(pairResult, best)) + best = pairResult; + } + if (best == null || best.Count == 0) return false; @@ -101,10 +120,20 @@ namespace OpenNest foreach (var config in configs) { - if (best == null || config.Count > best.Count) + if (IsBetterValidFill(config, best)) best = config; } + Debug.WriteLine($"[Fill(NestItem,Box)] Linear: {best?.Count ?? 0} parts | WorkArea: {workArea.Width:F1}x{workArea.Height:F1}"); + + // Try pair-based approach. + var pairResult = FillWithPairs(item, workArea); + + Debug.WriteLine($"[Fill(NestItem,Box)] Pair: {pairResult.Count} parts | Winner: {(IsBetterFill(pairResult, best) ? "Pair" : "Linear")}"); + + if (IsBetterFill(pairResult, best)) + best = pairResult; + if (best == null || best.Count == 0) return false; @@ -124,6 +153,18 @@ namespace OpenNest var angles = FindHullEdgeAngles(groupParts); var best = FillPattern(engine, groupParts, angles); + Debug.WriteLine($"[Fill(groupParts,Box)] Linear: {best?.Count ?? 0} parts | WorkArea: {workArea.Width:F1}x{workArea.Height:F1}"); + + if (groupParts.Count == 1) + { + var pairResult = FillWithPairs(new NestItem { Drawing = groupParts[0].BaseDrawing }, workArea); + + Debug.WriteLine($"[Fill(groupParts,Box)] Pair: {pairResult.Count} parts | Winner: {(IsBetterFill(pairResult, best) ? "Pair" : "Linear")}"); + + if (IsBetterFill(pairResult, best)) + best = pairResult; + } + if (best == null || best.Count == 0) return false; @@ -223,14 +264,84 @@ namespace OpenNest private List FillWithPairs(NestItem item) { - var finder = new BestFitFinder(Plate.Size.Width, Plate.Size.Height); - var tileResults = finder.FindAndTile(item.Drawing, Plate, Plate.PartSpacing); + return FillWithPairs(item, Plate.WorkArea()); + } - if (tileResults.Count == 0) - return new List(); + private List FillWithPairs(NestItem item, Box workArea) + { + IPairEvaluator evaluator = null; - var bestTile = tileResults[0]; - return ConvertTileResultToParts(bestTile, item.Drawing); + if (CreateEvaluator != null) + { + try { evaluator = CreateEvaluator(item.Drawing, Plate.PartSpacing); } + catch { /* GPU not available, fall back to geometry */ } + } + + var finder = new BestFitFinder(Plate.Size.Width, Plate.Size.Height, evaluator); + var bestFits = finder.FindBestFits(item.Drawing, Plate.PartSpacing, stepSize: 0.25); + + var keptResults = bestFits.Where(r => r.Keep).Take(50).ToList(); + Debug.WriteLine($"[FillWithPairs] Total: {bestFits.Count}, Kept: {bestFits.Count(r => r.Keep)}, Trying: {keptResults.Count}"); + + var resultBag = new System.Collections.Concurrent.ConcurrentBag<(int count, List parts)>(); + + System.Threading.Tasks.Parallel.For(0, keptResults.Count, i => + { + var result = keptResults[i]; + var pairParts = BuildPairParts(result, item.Drawing); + var angles = FindHullEdgeAngles(pairParts); + var engine = new FillLinear(workArea, Plate.PartSpacing); + var filled = FillPattern(engine, pairParts, angles); + + if (filled != null && filled.Count > 0) + resultBag.Add((filled.Count, filled)); + }); + + List best = null; + + foreach (var (count, parts) in resultBag) + { + if (best == null || count > best.Count) + best = parts; + } + + (evaluator as IDisposable)?.Dispose(); + + Debug.WriteLine($"[FillWithPairs] Best pair result: {best?.Count ?? 0} parts"); + return best ?? new List(); + } + + public static List BuildPairParts(BestFitResult bestFit, Drawing drawing) + { + var candidate = bestFit.Candidate; + + var part1 = new Part(drawing); + var bbox1 = part1.Program.BoundingBox(); + part1.Offset(-bbox1.Location.X, -bbox1.Location.Y); + part1.UpdateBounds(); + + var part2 = new Part(drawing); + if (!candidate.Part2Rotation.IsEqualTo(0)) + part2.Rotate(candidate.Part2Rotation); + var bbox2 = part2.Program.BoundingBox(); + part2.Offset(-bbox2.Location.X, -bbox2.Location.Y); + part2.Location = candidate.Part2Offset; + part2.UpdateBounds(); + + if (!bestFit.OptimalRotation.IsEqualTo(0)) + { + var pairBounds = ((IEnumerable)new IBoundable[] { part1, part2 }).GetBoundingBox(); + var center = pairBounds.Center; + part1.Rotate(-bestFit.OptimalRotation, center); + part2.Rotate(-bestFit.OptimalRotation, center); + } + + var finalBounds = ((IEnumerable)new IBoundable[] { part1, part2 }).GetBoundingBox(); + var offset = new Vector(-finalBounds.Left, -finalBounds.Bottom); + part1.Offset(offset); + part2.Offset(offset); + + return new List { part1, part2 }; } private List ConvertTileResultToParts(TileResult tileResult, Drawing drawing) @@ -295,6 +406,64 @@ namespace OpenNest return parts; } + private bool HasOverlaps(List parts, double spacing) + { + if (parts == null || parts.Count <= 1) + return false; + + for (var i = 0; i < parts.Count; i++) + { + for (var j = i + 1; j < parts.Count; j++) + { + List pts; + + if (parts[i].Intersects(parts[j], out pts)) + return true; + } + } + + return false; + } + + private bool IsBetterFill(List candidate, List current) + { + if (candidate == null || candidate.Count == 0) + return false; + + if (current == null || current.Count == 0) + return true; + + if (candidate.Count != current.Count) + return candidate.Count > current.Count; + + // Same count: prefer smaller bounding box (more compact). + var candidateBox = ((IEnumerable)candidate).GetBoundingBox(); + var currentBox = ((IEnumerable)current).GetBoundingBox(); + + return candidateBox.Area() < currentBox.Area(); + } + + private bool IsBetterValidFill(List candidate, List current) + { + if (candidate == null || candidate.Count == 0) + return false; + + // Reject candidate if it has overlapping parts. + if (HasOverlaps(candidate, Plate.PartSpacing)) + return false; + + if (current == null || current.Count == 0) + return true; + + if (candidate.Count != current.Count) + return candidate.Count > current.Count; + + var candidateBox = ((IEnumerable)candidate).GetBoundingBox(); + var currentBox = ((IEnumerable)current).GetBoundingBox(); + + return candidateBox.Area() < currentBox.Area(); + } + private List FindHullEdgeAngles(List parts) { var points = new List(); @@ -376,10 +545,10 @@ namespace OpenNest var h = engine.Fill(pattern, NestDirection.Horizontal); var v = engine.Fill(pattern, NestDirection.Vertical); - if (best == null || h.Count > best.Count) + if (IsBetterValidFill(h, best)) best = h; - if (best == null || v.Count > best.Count) + if (IsBetterValidFill(v, best)) best = v; } diff --git a/OpenNest/Actions/ActionClone.cs b/OpenNest/Actions/ActionClone.cs index 5879048..c0755a3 100644 --- a/OpenNest/Actions/ActionClone.cs +++ b/OpenNest/Actions/ActionClone.cs @@ -172,6 +172,7 @@ namespace OpenNest.Actions { var plate = plateView.Plate; var engine = new NestEngine(plate); + engine.CreateEvaluator = GpuEvaluatorFactory.Create; var groupParts = parts.Select(p => p.BasePart).ToList(); var bounds = plate.WorkArea(); diff --git a/OpenNest/Actions/ActionFillArea.cs b/OpenNest/Actions/ActionFillArea.cs index ba64af2..c17e39f 100644 --- a/OpenNest/Actions/ActionFillArea.cs +++ b/OpenNest/Actions/ActionFillArea.cs @@ -25,10 +25,8 @@ namespace OpenNest.Actions private void FillArea() { var engine = new NestEngine(plateView.Plate); - engine.FillArea(SelectedArea, new NestItem - { - Drawing = drawing - }); + engine.CreateEvaluator = GpuEvaluatorFactory.Create; + engine.Fill(new NestItem { Drawing = drawing }, SelectedArea); plateView.Invalidate(); Update(); diff --git a/OpenNest/Forms/MainForm.cs b/OpenNest/Forms/MainForm.cs index 16adabe..544da59 100644 --- a/OpenNest/Forms/MainForm.cs +++ b/OpenNest/Forms/MainForm.cs @@ -501,6 +501,33 @@ namespace OpenNest.Forms activeForm.PlateView.SetAction(typeof(ActionSelectArea)); } + private void BestFitViewer_Click(object sender, EventArgs e) + { + if (activeForm == null) + return; + + var plate = activeForm.PlateView.Plate; + var drawing = activeForm.Nest.Drawings.Count > 0 + ? activeForm.Nest.Drawings.First() + : null; + + if (drawing == null) + { + MessageBox.Show("No drawings available.", "Best-Fit Viewer", + MessageBoxButtons.OK, MessageBoxIcon.Information); + return; + } + + using (var form = new BestFitViewerForm(drawing, plate)) + { + if (form.ShowDialog(this) == DialogResult.OK && form.SelectedResult != null) + { + var parts = NestEngine.BuildPairParts(form.SelectedResult, drawing); + activeForm.PlateView.SetAction(typeof(ActionClone), parts); + } + } + } + private void SetOffsetIncrement_Click(object sender, EventArgs e) { if (activeForm == null) return; @@ -645,6 +672,7 @@ namespace OpenNest.Forms : activeForm.PlateView.Plate; var engine = new NestEngine(plate); + engine.CreateEvaluator = GpuEvaluatorFactory.Create; if (!engine.Pack(items)) break; @@ -718,6 +746,7 @@ namespace OpenNest.Forms return; var engine = new NestEngine(activeForm.PlateView.Plate); + engine.CreateEvaluator = GpuEvaluatorFactory.Create; engine.Fill(new NestItem { Drawing = drawing diff --git a/OpenNest/GpuEvaluatorFactory.cs b/OpenNest/GpuEvaluatorFactory.cs new file mode 100644 index 0000000..4993ac3 --- /dev/null +++ b/OpenNest/GpuEvaluatorFactory.cs @@ -0,0 +1,22 @@ +using System.Diagnostics; +using OpenNest.Engine.BestFit; +using OpenNest.Gpu; + +namespace OpenNest +{ + internal static class GpuEvaluatorFactory + { + public static IPairEvaluator Create(Drawing drawing, double spacing) + { + try + { + return new GpuPairEvaluator(drawing, spacing); + } + catch + { + Debug.WriteLine("[GpuEvaluatorFactory] GPU not available, falling back to CPU"); + return null; + } + } + } +} diff --git a/OpenNest/OpenNest.csproj b/OpenNest/OpenNest.csproj index d3a7593..4eafc18 100644 --- a/OpenNest/OpenNest.csproj +++ b/OpenNest/OpenNest.csproj @@ -13,6 +13,7 @@ +