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 <noreply@anthropic.com>
This commit is contained in:
2026-03-08 14:02:41 -04:00
parent 031264e98f
commit 3220306d3a
8 changed files with 126 additions and 28 deletions
+100
View File
@@ -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<CacheKey, List<BestFitResult>> _cache =
new ConcurrentDictionary<CacheKey, List<BestFitResult>>();
public static Func<Drawing, double, IPairEvaluator> CreateEvaluator { get; set; }
public static List<BestFitResult> 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<CacheKey>
{
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;
}
}
}
}
}
@@ -35,6 +35,16 @@ namespace OpenNest.Engine.BestFit
part1, part2Template, drawing, spacing, stepSize, part1, part2Template, drawing, spacing, stepSize,
PushDirection.Down, candidates, ref testNumber); 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; return candidates;
} }
@@ -77,11 +87,15 @@ namespace OpenNest.Engine.BestFit
{ {
var part2 = (Part)part2Template.Clone(); 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) if (isHorizontalPush)
part2.Offset(pushStartOffset, offset); part2.Offset(startPos, offset);
else else
part2.Offset(offset, pushStartOffset); part2.Offset(offset, startPos);
// Get part2's offset lines (half-spacing outward) // Get part2's offset lines (half-spacing outward)
var part2Lines = Helper.GetOffsetPartLines(part2, halfSpacing); var part2Lines = Helper.GetOffsetPartLines(part2, halfSpacing);
+4 -16
View File
@@ -1,5 +1,4 @@
using System; using System.Collections.Generic;
using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using OpenNest.Engine.BestFit; using OpenNest.Engine.BestFit;
@@ -20,8 +19,6 @@ namespace OpenNest
public NestDirection NestDirection { get; set; } public NestDirection NestDirection { get; set; }
public Func<Drawing, double, IPairEvaluator> CreateEvaluator { get; set; }
public bool Fill(NestItem item) public bool Fill(NestItem item)
{ {
return Fill(item, Plate.WorkArea()); return Fill(item, Plate.WorkArea());
@@ -151,16 +148,9 @@ namespace OpenNest
private List<Part> FillWithPairs(NestItem item, Box workArea) private List<Part> FillWithPairs(NestItem item, Box workArea)
{ {
IPairEvaluator evaluator = null; var bestFits = BestFitCache.GetOrCompute(
item.Drawing, Plate.Size.Width, Plate.Size.Height,
if (CreateEvaluator != null) Plate.PartSpacing);
{
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(); 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}"); Debug.WriteLine($"[FillWithPairs] Total: {bestFits.Count}, Kept: {bestFits.Count(r => r.Keep)}, Trying: {keptResults.Count}");
@@ -187,8 +177,6 @@ namespace OpenNest
best = parts; best = parts;
} }
(evaluator as IDisposable)?.Dispose();
Debug.WriteLine($"[FillWithPairs] Best pair result: {best?.Count ?? 0} parts"); Debug.WriteLine($"[FillWithPairs] Best pair result: {best?.Count ?? 0} parts");
return best ?? new List<Part>(); return best ?? new List<Part>();
} }
-2
View File
@@ -4,7 +4,6 @@ using System.Linq;
using System.Windows.Forms; using System.Windows.Forms;
using OpenNest.Controls; using OpenNest.Controls;
using OpenNest.Geometry; using OpenNest.Geometry;
using OpenNest.Gpu;
namespace OpenNest.Actions namespace OpenNest.Actions
{ {
@@ -173,7 +172,6 @@ namespace OpenNest.Actions
{ {
var plate = plateView.Plate; var plate = plateView.Plate;
var engine = new NestEngine(plate); var engine = new NestEngine(plate);
engine.CreateEvaluator = GpuEvaluatorFactory.Create;
var groupParts = parts.Select(p => p.BasePart).ToList(); var groupParts = parts.Select(p => p.BasePart).ToList();
var bounds = plate.WorkArea(); var bounds = plate.WorkArea();
-2
View File
@@ -1,7 +1,6 @@
using System.ComponentModel; using System.ComponentModel;
using System.Windows.Forms; using System.Windows.Forms;
using OpenNest.Controls; using OpenNest.Controls;
using OpenNest.Gpu;
namespace OpenNest.Actions namespace OpenNest.Actions
{ {
@@ -26,7 +25,6 @@ namespace OpenNest.Actions
private void FillArea() private void FillArea()
{ {
var engine = new NestEngine(plateView.Plate); var engine = new NestEngine(plateView.Plate);
engine.CreateEvaluator = GpuEvaluatorFactory.Create;
engine.Fill(new NestItem { Drawing = drawing }, SelectedArea); engine.Fill(new NestItem { Drawing = drawing }, SelectedArea);
plateView.Invalidate(); plateView.Invalidate();
+2 -3
View File
@@ -11,7 +11,6 @@ namespace OpenNest.Forms
private const int Columns = 5; private const int Columns = 5;
private const int RowHeight = 300; private const int RowHeight = 300;
private const int MaxResults = 50; 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 KeptColor = Color.FromArgb(0, 0, 100);
private static readonly Color DroppedColor = Color.FromArgb(100, 0, 0); private static readonly Color DroppedColor = Color.FromArgb(100, 0, 0);
@@ -56,8 +55,8 @@ namespace OpenNest.Forms
{ {
var sw = Stopwatch.StartNew(); var sw = Stopwatch.StartNew();
var finder = new BestFitFinder(plate.Size.Width, plate.Size.Height); var results = BestFitCache.GetOrCompute(
var results = finder.FindBestFits(drawing, plate.PartSpacing, ViewerStepSize); drawing, plate.Size.Width, plate.Size.Height, plate.PartSpacing);
var findMs = sw.ElapsedMilliseconds; var findMs = sw.ElapsedMilliseconds;
var total = results.Count; var total = results.Count;
-2
View File
@@ -688,7 +688,6 @@ namespace OpenNest.Forms
: activeForm.PlateView.Plate; : activeForm.PlateView.Plate;
var engine = new NestEngine(plate); var engine = new NestEngine(plate);
engine.CreateEvaluator = GpuEvaluatorFactory.Create;
if (!engine.Pack(items)) if (!engine.Pack(items))
break; break;
@@ -762,7 +761,6 @@ namespace OpenNest.Forms
return; return;
var engine = new NestEngine(activeForm.PlateView.Plate); var engine = new NestEngine(activeForm.PlateView.Plate);
engine.CreateEvaluator = GpuEvaluatorFactory.Create;
engine.Fill(new NestItem engine.Fill(new NestItem
{ {
Drawing = drawing Drawing = drawing
+3
View File
@@ -1,6 +1,8 @@
using System; using System;
using System.Windows.Forms; using System.Windows.Forms;
using OpenNest.Engine.BestFit;
using OpenNest.Forms; using OpenNest.Forms;
using OpenNest.Gpu;
namespace OpenNest namespace OpenNest
{ {
@@ -11,6 +13,7 @@ namespace OpenNest
{ {
Application.EnableVisualStyles(); Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false); Application.SetCompatibleTextRenderingDefault(false);
BestFitCache.CreateEvaluator = GpuEvaluatorFactory.Create;
Application.Run(new MainForm()); Application.Run(new MainForm());
} }
} }