From 3220306d3aacd435e09bdc19210fd6234795ec9c Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Sun, 8 Mar 2026 14:02:41 -0400 Subject: [PATCH] feat: add reverse push directions for concave interlocking and cache best-fit results Add PushDirection.Right and PushDirection.Up to RotationSlideStrategy so parts can approach from all four directions. This discovers concave interlocking arrangements (e.g. L-shaped parts nesting into each other's cavities) that the original Left/Down-only slides could never reach. Introduce BestFitCache so best-fit results are computed once at step size 0.25 and shared between the viewer and nesting engine. The GPU evaluator factory is configured once at startup instead of being wired per call site, and NestEngine.CreateEvaluator is removed. Co-Authored-By: Claude Opus 4.6 --- OpenNest.Engine/BestFit/BestFitCache.cs | 100 ++++++++++++++++++ .../BestFit/RotationSlideStrategy.cs | 20 +++- OpenNest.Engine/NestEngine.cs | 20 +--- OpenNest/Actions/ActionClone.cs | 2 - OpenNest/Actions/ActionFillArea.cs | 2 - OpenNest/Forms/BestFitViewerForm.cs | 5 +- OpenNest/Forms/MainForm.cs | 2 - OpenNest/MainApp.cs | 3 + 8 files changed, 126 insertions(+), 28 deletions(-) create mode 100644 OpenNest.Engine/BestFit/BestFitCache.cs diff --git a/OpenNest.Engine/BestFit/BestFitCache.cs b/OpenNest.Engine/BestFit/BestFitCache.cs new file mode 100644 index 0000000..04b7613 --- /dev/null +++ b/OpenNest.Engine/BestFit/BestFitCache.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace OpenNest.Engine.BestFit +{ + public static class BestFitCache + { + private const double StepSize = 0.25; + + private static readonly ConcurrentDictionary> _cache = + new ConcurrentDictionary>(); + + public static Func CreateEvaluator { get; set; } + + public static List GetOrCompute( + Drawing drawing, double plateWidth, double plateHeight, + double spacing) + { + var key = new CacheKey(drawing, plateWidth, plateHeight, spacing); + + if (_cache.TryGetValue(key, out var cached)) + return cached; + + IPairEvaluator evaluator = null; + + try + { + if (CreateEvaluator != null) + { + try { evaluator = CreateEvaluator(drawing, spacing); } + catch { /* fall back to default evaluator */ } + } + + var finder = new BestFitFinder(plateWidth, plateHeight, evaluator); + var results = finder.FindBestFits(drawing, spacing, StepSize); + + _cache.TryAdd(key, results); + return results; + } + finally + { + (evaluator as IDisposable)?.Dispose(); + } + } + + public static void Invalidate(Drawing drawing) + { + foreach (var key in _cache.Keys) + { + if (ReferenceEquals(key.Drawing, drawing)) + _cache.TryRemove(key, out _); + } + } + + public static void Clear() + { + _cache.Clear(); + } + + private readonly struct CacheKey : IEquatable + { + public readonly Drawing Drawing; + public readonly double PlateWidth; + public readonly double PlateHeight; + public readonly double Spacing; + + public CacheKey(Drawing drawing, double plateWidth, double plateHeight, double spacing) + { + Drawing = drawing; + PlateWidth = plateWidth; + PlateHeight = plateHeight; + Spacing = spacing; + } + + public bool Equals(CacheKey other) + { + return ReferenceEquals(Drawing, other.Drawing) && + PlateWidth == other.PlateWidth && + PlateHeight == other.PlateHeight && + Spacing == other.Spacing; + } + + public override bool Equals(object obj) => obj is CacheKey other && Equals(other); + + public override int GetHashCode() + { + unchecked + { + var hash = RuntimeHelpers.GetHashCode(Drawing); + hash = hash * 397 ^ PlateWidth.GetHashCode(); + hash = hash * 397 ^ PlateHeight.GetHashCode(); + hash = hash * 397 ^ Spacing.GetHashCode(); + return hash; + } + } + } + } +} diff --git a/OpenNest.Engine/BestFit/RotationSlideStrategy.cs b/OpenNest.Engine/BestFit/RotationSlideStrategy.cs index 6079385..4e09da2 100644 --- a/OpenNest.Engine/BestFit/RotationSlideStrategy.cs +++ b/OpenNest.Engine/BestFit/RotationSlideStrategy.cs @@ -35,6 +35,16 @@ namespace OpenNest.Engine.BestFit part1, part2Template, drawing, spacing, stepSize, PushDirection.Down, candidates, ref testNumber); + // 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); + return candidates; } @@ -77,11 +87,15 @@ namespace OpenNest.Engine.BestFit { var part2 = (Part)part2Template.Clone(); - // Place part2 far away along push axis, at perpendicular offset + // 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(pushStartOffset, offset); + part2.Offset(startPos, offset); else - part2.Offset(offset, pushStartOffset); + part2.Offset(offset, startPos); // Get part2's offset lines (half-spacing outward) var part2Lines = Helper.GetOffsetPartLines(part2, halfSpacing); diff --git a/OpenNest.Engine/NestEngine.cs b/OpenNest.Engine/NestEngine.cs index e829e2c..5b99476 100644 --- a/OpenNest.Engine/NestEngine.cs +++ b/OpenNest.Engine/NestEngine.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Diagnostics; using System.Linq; using OpenNest.Engine.BestFit; @@ -20,8 +19,6 @@ namespace OpenNest public NestDirection NestDirection { get; set; } - public Func CreateEvaluator { get; set; } - public bool Fill(NestItem item) { return Fill(item, Plate.WorkArea()); @@ -151,16 +148,9 @@ namespace OpenNest private List FillWithPairs(NestItem item, Box workArea) { - IPairEvaluator evaluator = null; - - 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 bestFits = BestFitCache.GetOrCompute( + item.Drawing, Plate.Size.Width, Plate.Size.Height, + Plate.PartSpacing); 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}"); @@ -187,8 +177,6 @@ namespace OpenNest best = parts; } - (evaluator as IDisposable)?.Dispose(); - Debug.WriteLine($"[FillWithPairs] Best pair result: {best?.Count ?? 0} parts"); return best ?? new List(); } diff --git a/OpenNest/Actions/ActionClone.cs b/OpenNest/Actions/ActionClone.cs index bcc6497..5879048 100644 --- a/OpenNest/Actions/ActionClone.cs +++ b/OpenNest/Actions/ActionClone.cs @@ -4,7 +4,6 @@ using System.Linq; using System.Windows.Forms; using OpenNest.Controls; using OpenNest.Geometry; -using OpenNest.Gpu; namespace OpenNest.Actions { @@ -173,7 +172,6 @@ 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 1a2cddf..aec5099 100644 --- a/OpenNest/Actions/ActionFillArea.cs +++ b/OpenNest/Actions/ActionFillArea.cs @@ -1,7 +1,6 @@ using System.ComponentModel; using System.Windows.Forms; using OpenNest.Controls; -using OpenNest.Gpu; namespace OpenNest.Actions { @@ -26,7 +25,6 @@ namespace OpenNest.Actions private void FillArea() { var engine = new NestEngine(plateView.Plate); - engine.CreateEvaluator = GpuEvaluatorFactory.Create; engine.Fill(new NestItem { Drawing = drawing }, SelectedArea); plateView.Invalidate(); diff --git a/OpenNest/Forms/BestFitViewerForm.cs b/OpenNest/Forms/BestFitViewerForm.cs index c801379..8d1f438 100644 --- a/OpenNest/Forms/BestFitViewerForm.cs +++ b/OpenNest/Forms/BestFitViewerForm.cs @@ -11,7 +11,6 @@ namespace OpenNest.Forms private const int Columns = 5; private const int RowHeight = 300; private const int MaxResults = 50; - private const double ViewerStepSize = 1.0; private static readonly Color KeptColor = Color.FromArgb(0, 0, 100); private static readonly Color DroppedColor = Color.FromArgb(100, 0, 0); @@ -56,8 +55,8 @@ namespace OpenNest.Forms { var sw = Stopwatch.StartNew(); - var finder = new BestFitFinder(plate.Size.Width, plate.Size.Height); - var results = finder.FindBestFits(drawing, plate.PartSpacing, ViewerStepSize); + var results = BestFitCache.GetOrCompute( + drawing, plate.Size.Width, plate.Size.Height, plate.PartSpacing); var findMs = sw.ElapsedMilliseconds; var total = results.Count; diff --git a/OpenNest/Forms/MainForm.cs b/OpenNest/Forms/MainForm.cs index 2628ac5..1b39959 100644 --- a/OpenNest/Forms/MainForm.cs +++ b/OpenNest/Forms/MainForm.cs @@ -688,7 +688,6 @@ namespace OpenNest.Forms : activeForm.PlateView.Plate; var engine = new NestEngine(plate); - engine.CreateEvaluator = GpuEvaluatorFactory.Create; if (!engine.Pack(items)) break; @@ -762,7 +761,6 @@ 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/MainApp.cs b/OpenNest/MainApp.cs index 67660c0..0669280 100644 --- a/OpenNest/MainApp.cs +++ b/OpenNest/MainApp.cs @@ -1,6 +1,8 @@ using System; using System.Windows.Forms; +using OpenNest.Engine.BestFit; using OpenNest.Forms; +using OpenNest.Gpu; namespace OpenNest { @@ -11,6 +13,7 @@ namespace OpenNest { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); + BestFitCache.CreateEvaluator = GpuEvaluatorFactory.Create; Application.Run(new MainForm()); } }