diff --git a/OpenNest.Engine/DefaultNestEngine.cs b/OpenNest.Engine/DefaultNestEngine.cs index 54c346e..0039bde 100644 --- a/OpenNest.Engine/DefaultNestEngine.cs +++ b/OpenNest.Engine/DefaultNestEngine.cs @@ -66,8 +66,15 @@ namespace OpenNest if (item.Quantity > 0 && best.Count > item.Quantity) best = ShrinkFiller.TrimToCount(best, item.Quantity, ShrinkAxis.Width); - ReportProgress(progress, WinnerPhase, PlateNumber, best, workArea, BuildProgressSummary(), - isOverallBest: true); + ReportProgress(progress, new ProgressReport + { + Phase = WinnerPhase, + PlateNumber = PlateNumber, + Parts = best, + WorkArea = workArea, + Description = BuildProgressSummary(), + IsOverallBest = true, + }); return best; } @@ -94,8 +101,15 @@ namespace OpenNest Debug.WriteLine($"[Fill(groupParts,Box)] Linear pattern: {best?.Count ?? 0} parts | WorkArea: {workArea.Width:F1}x{workArea.Length:F1}"); - ReportProgress(progress, NestPhase.Linear, PlateNumber, best, workArea, BuildProgressSummary(), - isOverallBest: true); + ReportProgress(progress, new ProgressReport + { + Phase = NestPhase.Linear, + PlateNumber = PlateNumber, + Parts = best, + WorkArea = workArea, + Description = BuildProgressSummary(), + IsOverallBest = true, + }); return best ?? new List(); } @@ -151,9 +165,15 @@ namespace OpenNest if (context.CurrentBest != null && context.CurrentBest.Count > 0) { - ReportProgress(context.Progress, context.WinnerPhase, PlateNumber, - context.CurrentBest, context.WorkArea, BuildProgressSummary(), - isOverallBest: true); + ReportProgress(context.Progress, new ProgressReport + { + Phase = context.WinnerPhase, + PlateNumber = PlateNumber, + Parts = context.CurrentBest, + WorkArea = context.WorkArea, + Description = BuildProgressSummary(), + IsOverallBest = true, + }); } } } diff --git a/OpenNest.Engine/Fill/AccumulatingProgress.cs b/OpenNest.Engine/Fill/AccumulatingProgress.cs index 051ec22..d1c2101 100644 --- a/OpenNest.Engine/Fill/AccumulatingProgress.cs +++ b/OpenNest.Engine/Fill/AccumulatingProgress.cs @@ -26,7 +26,6 @@ namespace OpenNest.Engine.Fill combined.AddRange(previousParts); combined.AddRange(value.BestParts); value.BestParts = combined; - value.BestPartCount = combined.Count; } inner.Report(value); diff --git a/OpenNest.Engine/Fill/FillExtents.cs b/OpenNest.Engine/Fill/FillExtents.cs index 185c346..6fae350 100644 --- a/OpenNest.Engine/Fill/FillExtents.cs +++ b/OpenNest.Engine/Fill/FillExtents.cs @@ -36,18 +36,36 @@ namespace OpenNest.Engine.Fill if (column.Count == 0) return new List(); - NestEngineBase.ReportProgress(progress, NestPhase.Extents, plateNumber, - column, workArea, $"Extents: initial column {column.Count} parts"); + NestEngineBase.ReportProgress(progress, new ProgressReport + { + Phase = NestPhase.Extents, + PlateNumber = plateNumber, + Parts = column, + WorkArea = workArea, + Description = $"Extents: initial column {column.Count} parts", + }); var adjusted = AdjustColumn(pair.Value, column, token); - NestEngineBase.ReportProgress(progress, NestPhase.Extents, plateNumber, - adjusted, workArea, $"Extents: adjusted column {adjusted.Count} parts"); + NestEngineBase.ReportProgress(progress, new ProgressReport + { + Phase = NestPhase.Extents, + PlateNumber = plateNumber, + Parts = adjusted, + WorkArea = workArea, + Description = $"Extents: adjusted column {adjusted.Count} parts", + }); var result = RepeatColumns(adjusted, token); - NestEngineBase.ReportProgress(progress, NestPhase.Extents, plateNumber, - result, workArea, $"Extents: {result.Count} parts total"); + NestEngineBase.ReportProgress(progress, new ProgressReport + { + Phase = NestPhase.Extents, + PlateNumber = plateNumber, + Parts = result, + WorkArea = workArea, + Description = $"Extents: {result.Count} parts total", + }); return result; } diff --git a/OpenNest.Engine/Fill/IterativeShrinkFiller.cs b/OpenNest.Engine/Fill/IterativeShrinkFiller.cs index e6de8b2..7b0722e 100644 --- a/OpenNest.Engine/Fill/IterativeShrinkFiller.cs +++ b/OpenNest.Engine/Fill/IterativeShrinkFiller.cs @@ -108,8 +108,15 @@ namespace OpenNest.Engine.Fill var allParts = new List(placedSoFar.Count + best.Count); allParts.AddRange(placedSoFar); allParts.AddRange(best); - NestEngineBase.ReportProgress(progress, NestPhase.Custom, plateNumber, - allParts, box, $"Shrink: {best.Count} parts placed", isOverallBest: true); + NestEngineBase.ReportProgress(progress, new ProgressReport + { + Phase = NestPhase.Custom, + PlateNumber = plateNumber, + Parts = allParts, + WorkArea = box, + Description = $"Shrink: {best.Count} parts placed", + IsOverallBest = true, + }); } // Accumulate for the next item's progress reports. diff --git a/OpenNest.Engine/Fill/PairFiller.cs b/OpenNest.Engine/Fill/PairFiller.cs index fa93dd6..bef8a90 100644 --- a/OpenNest.Engine/Fill/PairFiller.cs +++ b/OpenNest.Engine/Fill/PairFiller.cs @@ -88,8 +88,14 @@ namespace OpenNest.Engine.Fill sinceImproved++; } - NestEngineBase.ReportProgress(progress, NestPhase.Pairs, plateNumber, best, workArea, - $"Pairs: {i + 1}/{candidates.Count} candidates, best = {best?.Count ?? 0} parts"); + NestEngineBase.ReportProgress(progress, new ProgressReport + { + Phase = NestPhase.Pairs, + PlateNumber = plateNumber, + Parts = best, + WorkArea = workArea, + Description = $"Pairs: {i + 1}/{candidates.Count} candidates, best = {best?.Count ?? 0} parts", + }); if (i + 1 >= EarlyExitMinTried && sinceImproved >= EarlyExitStaleLimit) { diff --git a/OpenNest.Engine/Fill/ShrinkFiller.cs b/OpenNest.Engine/Fill/ShrinkFiller.cs index 3856305..19172a6 100644 --- a/OpenNest.Engine/Fill/ShrinkFiller.cs +++ b/OpenNest.Engine/Fill/ShrinkFiller.cs @@ -79,8 +79,14 @@ namespace OpenNest.Engine.Fill var desc = $"Shrink {axis}: {bestParts.Count} parts, dim={dim:F1}"; - NestEngineBase.ReportProgress(progress, NestPhase.Custom, plateNumber, - allParts, workArea, desc); + NestEngineBase.ReportProgress(progress, new ProgressReport + { + Phase = NestPhase.Custom, + PlateNumber = plateNumber, + Parts = allParts, + WorkArea = workArea, + Description = desc, + }); } /// diff --git a/OpenNest.Engine/Fill/StripeFiller.cs b/OpenNest.Engine/Fill/StripeFiller.cs index 377d9e0..66b7728 100644 --- a/OpenNest.Engine/Fill/StripeFiller.cs +++ b/OpenNest.Engine/Fill/StripeFiller.cs @@ -93,9 +93,14 @@ public class StripeFiller } } - NestEngineBase.ReportProgress(_context.Progress, NestPhase.Custom, - _context.PlateNumber, bestParts, workArea, - $"{strategyName}: {i + 1}/{bestFits.Count} pairs, best = {bestParts?.Count ?? 0} parts"); + NestEngineBase.ReportProgress(_context.Progress, new ProgressReport + { + Phase = NestPhase.Custom, + PlateNumber = _context.PlateNumber, + Parts = bestParts, + WorkArea = workArea, + Description = $"{strategyName}: {i + 1}/{bestFits.Count} pairs, best = {bestParts?.Count ?? 0} parts", + }); } return bestParts ?? new List(); diff --git a/OpenNest.Engine/NestEngineBase.cs b/OpenNest.Engine/NestEngineBase.cs index 909e772..eaf58e8 100644 --- a/OpenNest.Engine/NestEngineBase.cs +++ b/OpenNest.Engine/NestEngineBase.cs @@ -210,55 +210,26 @@ namespace OpenNest // --- Protected utilities --- internal static void ReportProgress( - IProgress progress, - NestPhase phase, - int plateNumber, - List best, - Box workArea, - string description, - bool isOverallBest = false) + IProgress progress, ProgressReport report) { - if (progress == null || best == null || best.Count == 0) + if (progress == null || report.Parts == null || report.Parts.Count == 0) return; - var score = FillScore.Compute(best, workArea); - var clonedParts = new List(best.Count); - var totalPartArea = 0.0; - - foreach (var part in best) - { + var clonedParts = new List(report.Parts.Count); + foreach (var part in report.Parts) clonedParts.Add((Part)part.Clone()); - totalPartArea += part.BaseDrawing.Area; - } - var bounds = best.GetBoundingBox(); - - var msg = $"[Progress] Phase={phase}, Plate={plateNumber}, Parts={score.Count}, " + - $"Density={score.Density:P1}, Nested={bounds.Width:F1}x{bounds.Length:F1}, " + - $"PartArea={totalPartArea:F0}, Remnant={workArea.Area() - totalPartArea:F0}, " + - $"WorkArea={workArea.Width:F1}x{workArea.Length:F1} | {description}"; - Debug.WriteLine(msg); - try - { - System.IO.File.AppendAllText( - System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "nest-debug.log"), - $"{DateTime.Now:HH:mm:ss.fff} {msg}\n"); - } - catch { } + Debug.WriteLine($"[Progress] Phase={report.Phase}, Plate={report.PlateNumber}, " + + $"Parts={clonedParts.Count} | {report.Description}"); progress.Report(new NestProgress { - Phase = phase, - PlateNumber = plateNumber, - BestPartCount = score.Count, - BestDensity = score.Density, - NestedWidth = bounds.Width, - NestedLength = bounds.Length, - NestedArea = totalPartArea, + Phase = report.Phase, + PlateNumber = report.PlateNumber, BestParts = clonedParts, - Description = description, - ActiveWorkArea = workArea, - IsOverallBest = isOverallBest, + Description = report.Description, + ActiveWorkArea = report.WorkArea, + IsOverallBest = report.IsOverallBest, }); } @@ -270,7 +241,7 @@ namespace OpenNest var parts = new List(PhaseResults.Count); foreach (var r in PhaseResults) - parts.Add($"{FormatPhaseName(r.Phase)}: {r.PartCount}"); + parts.Add($"{r.Phase.ShortName()}: {r.PartCount}"); return string.Join(" | ", parts); } @@ -323,17 +294,5 @@ namespace OpenNest return false; } - protected static string FormatPhaseName(NestPhase phase) - { - switch (phase) - { - case NestPhase.Pairs: return "Pairs"; - case NestPhase.Linear: return "Linear"; - case NestPhase.RectBestFit: return "BestFit"; - case NestPhase.Extents: return "Extents"; - case NestPhase.Custom: return "Custom"; - default: return phase.ToString(); - } - } } } diff --git a/OpenNest.Engine/NestProgress.cs b/OpenNest.Engine/NestProgress.cs index 0a6801f..ce68b83 100644 --- a/OpenNest.Engine/NestProgress.cs +++ b/OpenNest.Engine/NestProgress.cs @@ -70,18 +70,93 @@ namespace OpenNest public int PartCount { get; set; } } + internal readonly struct ProgressReport + { + public NestPhase Phase { get; init; } + public int PlateNumber { get; init; } + public List Parts { get; init; } + public Box WorkArea { get; init; } + public string Description { get; init; } + public bool IsOverallBest { get; init; } + } + public class NestProgress { public NestPhase Phase { get; set; } public int PlateNumber { get; set; } - public int BestPartCount { get; set; } - public double BestDensity { get; set; } - public double NestedWidth { get; set; } - public double NestedLength { get; set; } - public double NestedArea { get; set; } - public List BestParts { get; set; } + + private List bestParts; + public List BestParts + { + get => bestParts; + set { bestParts = value; cachedParts = null; } + } + public string Description { get; set; } public Box ActiveWorkArea { get; set; } public bool IsOverallBest { get; set; } + + public int BestPartCount => BestParts?.Count ?? 0; + + private List cachedParts; + private Box cachedBounds; + private double cachedPartArea; + + private void EnsureCache() + { + if (cachedParts == bestParts) return; + cachedParts = bestParts; + if (bestParts == null || bestParts.Count == 0) + { + cachedBounds = default; + cachedPartArea = 0; + return; + } + cachedBounds = bestParts.GetBoundingBox(); + cachedPartArea = 0; + foreach (var p in bestParts) + cachedPartArea += p.BaseDrawing.Area; + } + + public double BestDensity + { + get + { + if (BestParts == null || BestParts.Count == 0) return 0; + EnsureCache(); + var bboxArea = cachedBounds.Width * cachedBounds.Length; + return bboxArea > 0 ? cachedPartArea / bboxArea : 0; + } + } + + public double NestedWidth + { + get + { + if (BestParts == null || BestParts.Count == 0) return 0; + EnsureCache(); + return cachedBounds.Width; + } + } + + public double NestedLength + { + get + { + if (BestParts == null || BestParts.Count == 0) return 0; + EnsureCache(); + return cachedBounds.Length; + } + } + + public double NestedArea + { + get + { + if (BestParts == null || BestParts.Count == 0) return 0; + EnsureCache(); + return cachedPartArea; + } + } } } diff --git a/OpenNest.Engine/Nfp/AutoNester.cs b/OpenNest.Engine/Nfp/AutoNester.cs index 9b94871..af371a1 100644 --- a/OpenNest.Engine/Nfp/AutoNester.cs +++ b/OpenNest.Engine/Nfp/AutoNester.cs @@ -74,8 +74,15 @@ namespace OpenNest.Engine.Nfp Debug.WriteLine($"[AutoNest] Result: {parts.Count} parts placed, {result.Iterations} SA iterations"); - NestEngineBase.ReportProgress(progress, NestPhase.Nfp, 0, parts, workArea, - $"NFP: {parts.Count} parts, {result.Iterations} iterations", isOverallBest: true); + NestEngineBase.ReportProgress(progress, new ProgressReport + { + Phase = NestPhase.Nfp, + PlateNumber = 0, + Parts = parts, + WorkArea = workArea, + Description = $"NFP: {parts.Count} parts, {result.Iterations} iterations", + IsOverallBest = true, + }); return parts; } diff --git a/OpenNest.Engine/Nfp/SimulatedAnnealing.cs b/OpenNest.Engine/Nfp/SimulatedAnnealing.cs index d84f1b8..8f93721 100644 --- a/OpenNest.Engine/Nfp/SimulatedAnnealing.cs +++ b/OpenNest.Engine/Nfp/SimulatedAnnealing.cs @@ -277,8 +277,15 @@ namespace OpenNest.Engine.Nfp private static void ReportBest(IProgress progress, List parts, Box workArea, string description) { - NestEngineBase.ReportProgress(progress, NestPhase.Nfp, 0, parts, workArea, - description, isOverallBest: true); + NestEngineBase.ReportProgress(progress, new ProgressReport + { + Phase = NestPhase.Nfp, + PlateNumber = 0, + Parts = parts, + WorkArea = workArea, + Description = description, + IsOverallBest = true, + }); } } } diff --git a/OpenNest.Engine/Strategies/LinearFillStrategy.cs b/OpenNest.Engine/Strategies/LinearFillStrategy.cs index b37f783..a0af093 100644 --- a/OpenNest.Engine/Strategies/LinearFillStrategy.cs +++ b/OpenNest.Engine/Strategies/LinearFillStrategy.cs @@ -47,9 +47,14 @@ namespace OpenNest.Engine.Strategies best = result; } - NestEngineBase.ReportProgress(context.Progress, NestPhase.Linear, - context.PlateNumber, best, workArea, - $"Linear: {ai + 1}/{angles.Count} angles, {angleDeg:F0}° best = {best?.Count ?? 0} parts"); + NestEngineBase.ReportProgress(context.Progress, new ProgressReport + { + Phase = NestPhase.Linear, + PlateNumber = context.PlateNumber, + Parts = best, + WorkArea = workArea, + Description = $"Linear: {ai + 1}/{angles.Count} angles, {angleDeg:F0}° best = {best?.Count ?? 0} parts", + }); } return best ?? new List(); diff --git a/OpenNest.Tests/AccumulatingProgressTests.cs b/OpenNest.Tests/AccumulatingProgressTests.cs index 870e859..52bd84d 100644 --- a/OpenNest.Tests/AccumulatingProgressTests.cs +++ b/OpenNest.Tests/AccumulatingProgressTests.cs @@ -18,7 +18,7 @@ public class AccumulatingProgressTests var accumulating = new AccumulatingProgress(inner, previous); var newParts = new List { TestHelpers.MakePartAt(20, 0, 10) }; - accumulating.Report(new NestProgress { BestParts = newParts, BestPartCount = 1 }); + accumulating.Report(new NestProgress { BestParts = newParts }); Assert.NotNull(inner.Last); Assert.Equal(2, inner.Last.BestParts.Count); @@ -32,7 +32,7 @@ public class AccumulatingProgressTests var accumulating = new AccumulatingProgress(inner, new List()); var newParts = new List { TestHelpers.MakePartAt(0, 0, 10) }; - accumulating.Report(new NestProgress { BestParts = newParts, BestPartCount = 1 }); + accumulating.Report(new NestProgress { BestParts = newParts }); Assert.NotNull(inner.Last); Assert.Single(inner.Last.BestParts); diff --git a/OpenNest.Tests/NestProgressTests.cs b/OpenNest.Tests/NestProgressTests.cs new file mode 100644 index 0000000..89a467e --- /dev/null +++ b/OpenNest.Tests/NestProgressTests.cs @@ -0,0 +1,100 @@ +using OpenNest.Geometry; + +namespace OpenNest.Tests; + +public class NestProgressTests +{ + [Fact] + public void BestPartCount_NullParts_ReturnsZero() + { + var progress = new NestProgress { BestParts = null }; + Assert.Equal(0, progress.BestPartCount); + } + + [Fact] + public void BestPartCount_ReturnsBestPartsCount() + { + var parts = new List + { + TestHelpers.MakePartAt(0, 0, 5), + TestHelpers.MakePartAt(10, 0, 5), + }; + var progress = new NestProgress { BestParts = parts }; + Assert.Equal(2, progress.BestPartCount); + } + + [Fact] + public void BestDensity_NullParts_ReturnsZero() + { + var progress = new NestProgress { BestParts = null }; + Assert.Equal(0, progress.BestDensity); + } + + [Fact] + public void BestDensity_MatchesFillScoreFormula() + { + var parts = new List + { + TestHelpers.MakePartAt(0, 0, 5), + TestHelpers.MakePartAt(5, 0, 5), + }; + var workArea = new Box(0, 0, 100, 100); + var progress = new NestProgress { BestParts = parts, ActiveWorkArea = workArea }; + Assert.Equal(1.0, progress.BestDensity, precision: 4); + } + + [Fact] + public void NestedWidth_ReturnsPartsSpan() + { + var parts = new List + { + TestHelpers.MakePartAt(0, 0, 5), + TestHelpers.MakePartAt(10, 0, 5), + }; + var progress = new NestProgress { BestParts = parts }; + Assert.Equal(15, progress.NestedWidth, precision: 4); + } + + [Fact] + public void NestedLength_ReturnsPartsSpan() + { + var parts = new List + { + TestHelpers.MakePartAt(0, 0, 5), + TestHelpers.MakePartAt(0, 10, 5), + }; + var progress = new NestProgress { BestParts = parts }; + Assert.Equal(15, progress.NestedLength, precision: 4); + } + + [Fact] + public void NestedArea_ReturnsSumOfPartAreas() + { + var parts = new List + { + TestHelpers.MakePartAt(0, 0, 5), + TestHelpers.MakePartAt(10, 0, 5), + }; + var progress = new NestProgress { BestParts = parts }; + Assert.Equal(50, progress.NestedArea, precision: 4); + } + + [Fact] + public void SettingBestParts_InvalidatesCache() + { + var parts1 = new List { TestHelpers.MakePartAt(0, 0, 5) }; + var parts2 = new List + { + TestHelpers.MakePartAt(0, 0, 5), + TestHelpers.MakePartAt(10, 0, 5), + }; + + var progress = new NestProgress { BestParts = parts1 }; + Assert.Equal(1, progress.BestPartCount); + Assert.Equal(25, progress.NestedArea, precision: 4); + + progress.BestParts = parts2; + Assert.Equal(2, progress.BestPartCount); + Assert.Equal(50, progress.NestedArea, precision: 4); + } +}