Files
OpenNest/OpenNest.Engine/BestFit/BestFitCache.cs
AJ Isaacs 3c59da17c2 fix(engine): fix pair candidate filtering for narrow plates and strips
The BestFitFilter's aspect ratio cap of 5.0 was rejecting valid pair
candidates needed for narrow plates (e.g. 60x6.5, aspect 9.2) and
remainder strips on normal plates. Three fixes:

- BestFitFinder: derive MaxAspectRatio from the plate's own aspect
  ratio so narrow plates don't reject all elongated pairs
- SelectPairCandidates: search the full unfiltered candidate list
  (not just Keep=true) in strip mode, so pairs rejected by aspect
  ratio for the main plate can still be used for narrow remainder
  strips
- BestFitCache.Populate: skip caching empty result lists so stale
  pre-computed data from nest files doesn't prevent recomputation

Also fixes console --size parsing to use LxW format matching
Size.Parse convention, and includes prior engine refactoring
(sequential fill loops, parallel FillPattern, pre-sorted edge
arrays in RotationSlideStrategy).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 01:14:07 -04:00

219 lines
7.8 KiB
C#

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 Func<ISlideComputer> CreateSlideComputer { 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;
ISlideComputer slideComputer = null;
try
{
if (CreateEvaluator != null)
{
try { evaluator = CreateEvaluator(drawing, spacing); }
catch { /* fall back to default 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);
return results;
}
finally
{
(evaluator as IDisposable)?.Dispose();
// Slide computer is managed by the factory as a singleton — don't dispose here
}
}
public static void ComputeForSizes(
Drawing drawing, double spacing,
IEnumerable<(double Width, double Height)> plateSizes)
{
// Skip sizes that are already cached.
var needed = new List<(double Width, double Height)>();
foreach (var size in plateSizes)
{
var key = new CacheKey(drawing, size.Width, size.Height, spacing);
if (!_cache.ContainsKey(key))
needed.Add(size);
}
if (needed.Count == 0)
return;
// Find the largest plate to use for the initial computation — this
// keeps the filter maximally permissive so we don't discard results
// that a smaller plate might still use after re-filtering.
var maxWidth = 0.0;
var maxHeight = 0.0;
foreach (var size in needed)
{
if (size.Width > maxWidth) maxWidth = size.Width;
if (size.Height > maxHeight) maxHeight = size.Height;
}
IPairEvaluator evaluator = null;
ISlideComputer slideComputer = null;
try
{
if (CreateEvaluator != null)
{
try { evaluator = CreateEvaluator(drawing, spacing); }
catch { /* fall back to default evaluator */ }
}
if (CreateSlideComputer != null)
{
try { slideComputer = CreateSlideComputer(); }
catch { /* fall back to CPU slide computation */ }
}
// Compute candidates and evaluate once with the largest plate.
var finder = new BestFitFinder(maxWidth, maxHeight, evaluator, slideComputer);
var baseResults = finder.FindBestFits(drawing, spacing, StepSize);
// Cache a filtered copy for each plate size.
foreach (var size in needed)
{
var filter = new BestFitFilter
{
MaxPlateWidth = size.Width,
MaxPlateHeight = size.Height
};
var copy = new List<BestFitResult>(baseResults.Count);
for (var i = 0; i < baseResults.Count; i++)
{
var r = baseResults[i];
copy.Add(new BestFitResult
{
Candidate = r.Candidate,
RotatedArea = r.RotatedArea,
BoundingWidth = r.BoundingWidth,
BoundingHeight = r.BoundingHeight,
OptimalRotation = r.OptimalRotation,
TrueArea = r.TrueArea,
HullAngles = r.HullAngles,
Keep = r.Keep,
Reason = r.Reason
});
}
filter.Apply(copy);
var key = new CacheKey(drawing, size.Width, size.Height, spacing);
_cache.TryAdd(key, copy);
}
}
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 Populate(Drawing drawing, double plateWidth, double plateHeight,
double spacing, List<BestFitResult> results)
{
if (results == null || results.Count == 0)
return;
var key = new CacheKey(drawing, plateWidth, plateHeight, spacing);
_cache.TryAdd(key, results);
}
public static Dictionary<(double PlateWidth, double PlateHeight, double Spacing), List<BestFitResult>>
GetAllForDrawing(Drawing drawing)
{
var result = new Dictionary<(double, double, double), List<BestFitResult>>();
foreach (var kvp in _cache)
{
if (ReferenceEquals(kvp.Key.Drawing, drawing))
result[(kvp.Key.PlateWidth, kvp.Key.PlateHeight, kvp.Key.Spacing)] = kvp.Value;
}
return result;
}
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;
}
}
}
}
}