using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using OpenNest.Engine.Jobs;
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.
///
/// A drawing reconstructed from job geometry () draws its
/// candidates from a process-wide pool keyed by the geometry's exact content and the spacing,
/// so every later job of the same geometry, in any engine instance or Auto Nest run, skips the
/// search. Candidates depend on the program geometry and spacing alone, so a pooled set is
/// the set the search would produce. The pool keeps the most
/// recently used sets and outlives the drawings; never touches it.
///
///
public static class BestFitCache
{
private const double StepSize = 0.25;
private static readonly ConditionalWeakTable _entries = new();
private static readonly object _entriesLock = new();
private static readonly ConditionalWeakTable _geometry = new();
private static readonly object _poolLock = new();
private static readonly LinkedList _poolOrder = new();
private static readonly Dictionary<(PartGeometrySnapshot Geometry, double Spacing), LinkedListNode>
_pool = new(new PoolKeyComparer());
private static int _sharedCapacity = 512;
public static Func CreateEvaluator { get; set; }
public static Func CreateSlideComputer { get; set; }
///
/// Candidate sets (one per geometry and spacing) the process-wide pool keeps; the least
/// recently used set is dropped first. Zero disables pooling for later computations.
///
public static int SharedCapacity
{
get => Volatile.Read(ref _sharedCapacity);
set
{
if (value < 0)
throw new ArgumentOutOfRangeException(nameof(value));
lock (_poolLock)
{
_sharedCapacity = value;
Trim();
}
}
}
///
/// Declares an exact reconstruction of
/// (its program unrotated, as DrawingJobMapper.ToProgram builds it). Its unfiltered
/// candidates then come from, and go to, the process-wide pool for that geometry content
/// and spacing. The declaration lapses if the drawing's
/// instance is replaced.
///
public static void ShareByGeometry(Drawing drawing, PartGeometrySnapshot geometry)
{
ArgumentNullException.ThrowIfNull(drawing);
ArgumentNullException.ThrowIfNull(geometry);
_geometry.AddOrUpdate(drawing, new SharedGeometry(geometry, drawing.Program));
}
///
/// True when the pool holds computed candidates for this geometry content and spacing, so
/// a drawing sharing it would skip the search.
///
public static bool IsShared(PartGeometrySnapshot geometry, double spacing)
{
ArgumentNullException.ThrowIfNull(geometry);
lock (_poolLock)
return _pool.TryGetValue((geometry, spacing), out var node)
&& node.Value.Candidates.IsValueCreated;
}
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));
}
///
/// Like , but cancelling stops
/// the wait, not the computation: it finishes in the background and caches its result.
/// Callers must pass a drawing nothing else mutates, such as a canonical copy.
///
public static List GetOrCompute(
Drawing drawing,
double plateWidth,
double plateHeight,
double spacing,
CancellationToken token
)
{
if (!token.CanBeCanceled)
return GetOrCompute(drawing, plateWidth, plateHeight, spacing);
token.ThrowIfCancellationRequested();
return Task.Run(() => GetOrCompute(drawing, plateWidth, plateHeight, spacing))
.WaitAsync(token)
.GetAwaiter()
.GetResult();
}
/// True when would return at once.
public static bool IsCached(Drawing drawing, double plateWidth, double plateHeight, double spacing)
{
var source = CanonicalFrame.SourceOf(drawing);
return _entries.TryGetValue(source, out var entry)
&& entry.IsCurrentFor(source)
&& entry.Filtered.ContainsKey((plateWidth, plateHeight, spacing));
}
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;
}
/// Drops every drawing's entry and the shared pool.
public static void Clear()
{
lock (_entriesLock)
_entries.Clear();
lock (_poolLock)
{
_pool.Clear();
_poolOrder.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 shared = SharedGeometryOf(CanonicalFrame.SourceOf(drawing));
var lazy = entry.Unfiltered.GetOrAdd(
spacing,
_ =>
{
var computation = new Lazy>(
() => ComputeCandidates(drawing, spacing),
LazyThreadSafetyMode.ExecutionAndPublication
);
return shared == null ? computation : PoolGetOrAdd(shared, spacing, computation);
}
);
try
{
return lazy.Value;
}
catch
{
// Don't cache the failure; the next caller retries.
entry.Unfiltered.TryRemove(new KeyValuePair>>(spacing, lazy));
if (shared != null)
PoolRemove(shared, spacing, lazy);
throw;
}
}
private static PartGeometrySnapshot SharedGeometryOf(Drawing source)
{
if (source == null || !_geometry.TryGetValue(source, out var shared))
return null;
// A replaced program no longer matches the declared geometry.
return ReferenceEquals(shared.Program, source.Program) ? shared.Geometry : null;
}
/// The pooled computation for this geometry and spacing, adding
/// when there is none; a hit becomes most recently used.
private static Lazy> PoolGetOrAdd(
PartGeometrySnapshot geometry,
double spacing,
Lazy> computation
)
{
lock (_poolLock)
{
var key = (geometry, spacing);
if (_pool.TryGetValue(key, out var node))
{
_poolOrder.Remove(node);
_poolOrder.AddFirst(node);
return node.Value.Candidates;
}
if (_sharedCapacity == 0)
return computation;
node = _poolOrder.AddFirst(new PoolEntry(geometry, spacing, computation));
_pool[key] = node;
Trim();
return computation;
}
}
private static void PoolRemove(PartGeometrySnapshot geometry, double spacing, Lazy> computation)
{
lock (_poolLock)
{
var key = (geometry, spacing);
if (_pool.TryGetValue(key, out var node) && ReferenceEquals(node.Value.Candidates, computation))
{
_pool.Remove(key);
_poolOrder.Remove(node);
}
}
}
// Caller holds _poolLock.
private static void Trim()
{
while (_poolOrder.Count > _sharedCapacity)
{
var last = _poolOrder.Last;
_pool.Remove((last.Value.Geometry, last.Value.Spacing));
_poolOrder.RemoveLast();
}
}
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);
}
/// The geometry a drawing was reconstructed from, and the program built then.
private sealed record SharedGeometry(PartGeometrySnapshot Geometry, CNC.Program Program);
private sealed record PoolEntry(
PartGeometrySnapshot Geometry,
double Spacing,
Lazy> Candidates
);
private sealed class PoolKeyComparer
: IEqualityComparer<(PartGeometrySnapshot Geometry, double Spacing)>
{
public bool Equals(
(PartGeometrySnapshot Geometry, double Spacing) x,
(PartGeometrySnapshot Geometry, double Spacing) y
) =>
x.Spacing.Equals(y.Spacing)
&& PartGeometrySnapshot.ContentComparer.Equals(x.Geometry, y.Geometry);
public int GetHashCode((PartGeometrySnapshot Geometry, double Spacing) key) =>
HashCode.Combine(PartGeometrySnapshot.ContentComparer.GetHashCode(key.Geometry), key.Spacing);
}
}
}