using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
namespace OpenNest.Engine.BestFit
{
///
/// Best-fit pair results per drawing. Entries are keyed weakly by the source drawing (canonical
/// copies resolve through ), so every fill of one drawing
/// shares them and closed nests or finished solves release theirs. Candidates are computed once
/// per spacing and filtered per plate size. An entry is dropped when the drawing's
/// instance or canonical angle changes.
///
public static class BestFitCache
{
private const double StepSize = 0.25;
private static readonly ConditionalWeakTable _entries = new();
private static readonly object _entriesLock = new();
public static Func CreateEvaluator { get; set; }
public static Func CreateSlideComputer { get; set; }
public static List GetOrCompute(
Drawing drawing,
double plateWidth,
double plateHeight,
double spacing
)
{
var entry = GetEntry(drawing);
var key = (plateWidth, plateHeight, spacing);
if (entry.Filtered.TryGetValue(key, out var cached))
return cached;
var candidates = GetCandidates(entry, drawing, spacing);
return entry.Filtered.GetOrAdd(key, _ => FilterForSize(candidates, plateWidth, plateHeight));
}
public static void ComputeForSizes(
Drawing drawing,
double spacing,
IEnumerable<(double Width, double Height)> plateSizes
)
{
foreach (var size in plateSizes)
GetOrCompute(drawing, size.Width, size.Height, spacing);
}
public static void Invalidate(Drawing drawing)
{
lock (_entriesLock)
_entries.Remove(CanonicalFrame.SourceOf(drawing));
}
public static void Populate(
Drawing drawing,
double plateWidth,
double plateHeight,
double spacing,
List results
)
{
if (results == null || results.Count == 0)
return;
GetEntry(drawing).Filtered.TryAdd((plateWidth, plateHeight, spacing), results);
}
public static Dictionary<
(double PlateWidth, double PlateHeight, double Spacing),
List
> GetAllForDrawing(Drawing drawing)
{
var result = new Dictionary<(double, double, double), List>();
var source = CanonicalFrame.SourceOf(drawing);
if (_entries.TryGetValue(source, out var entry) && entry.IsCurrentFor(source))
{
foreach (var kvp in entry.Filtered)
result[kvp.Key] = kvp.Value;
}
return result;
}
public static void Clear()
{
lock (_entriesLock)
_entries.Clear();
}
private static Entry GetEntry(Drawing drawing)
{
var source = CanonicalFrame.SourceOf(drawing);
if (_entries.TryGetValue(source, out var entry) && entry.IsCurrentFor(source))
return entry;
lock (_entriesLock)
{
if (_entries.TryGetValue(source, out entry) && entry.IsCurrentFor(source))
return entry;
entry = new Entry(source);
_entries.AddOrUpdate(source, entry);
return entry;
}
}
private static List GetCandidates(Entry entry, Drawing drawing, double spacing)
{
var lazy = entry.Unfiltered.GetOrAdd(
spacing,
_ => new Lazy>(
() => ComputeCandidates(drawing, spacing),
LazyThreadSafetyMode.ExecutionAndPublication
)
);
try
{
return lazy.Value;
}
catch
{
// Don't cache the failure; the next caller retries.
entry.Unfiltered.TryRemove(new KeyValuePair>>(spacing, lazy));
throw;
}
}
private static List ComputeCandidates(Drawing drawing, double spacing)
{
// Operate on the canonical frame so cached pair positions are orientation-invariant.
var canonical = CanonicalFrame.AsCanonicalCopy(drawing);
IPairEvaluator evaluator = null;
ISlideComputer slideComputer = null;
try
{
if (CreateEvaluator != null)
{
try
{
evaluator = CreateEvaluator(canonical, spacing);
}
catch
{ /* fall back to default evaluator */
}
}
if (CreateSlideComputer != null)
{
try
{
slideComputer = CreateSlideComputer();
}
catch
{ /* fall back to CPU slide computation */
}
}
// The plate size only feeds the filter, which FindCandidates skips.
var finder = new BestFitFinder(0, 0, evaluator, slideComputer);
return finder.FindCandidates(canonical, spacing, StepSize);
}
finally
{
(evaluator as IDisposable)?.Dispose();
// Slide computer is managed by the factory as a singleton — don't dispose here
}
}
private static List FilterForSize(
List candidates,
double plateWidth,
double plateHeight
)
{
var copy = new List(candidates.Count);
for (var i = 0; i < candidates.Count; i++)
{
var r = candidates[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,
}
);
}
BestFitFinder.CreateFilter(plateWidth, plateHeight).Apply(copy);
return copy;
}
private sealed class Entry
{
private readonly CNC.Program _program;
private readonly double _sourceAngle;
public readonly ConcurrentDictionary>> Unfiltered = new();
public readonly ConcurrentDictionary<
(double PlateWidth, double PlateHeight, double Spacing),
List
> Filtered = new();
public Entry(Drawing source)
{
_program = source.Program;
_sourceAngle = source.Source?.Angle ?? 0.0;
}
public bool IsCurrentFor(Drawing source) =>
ReferenceEquals(_program, source.Program)
&& _sourceAngle == (source.Source?.Angle ?? 0.0);
}
}
}