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); } } }