Compare commits

..

8 Commits

Author SHA1 Message Date
aj a548d5329a chore: update NestProgressForm designer layout
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 23:09:07 -04:00
aj 07012033c7 feat: use direction-specific engines in StripNestEngine
Height shrink now uses HorizontalRemnantEngine (minimizes Y-extent)
and width shrink uses VerticalRemnantEngine (minimizes X-extent).
IterativeShrinkFiller accepts an optional widthFillFunc so each
shrink axis can use a different fill engine.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 23:09:02 -04:00
aj 92b17b2963 perf: parallelize PairFiller candidates and add GridDedup
- Evaluate pair candidates in parallel batches instead of sequentially
- Add GridDedup to skip duplicate pattern/direction/workArea combos
  across PairFiller and StripeFiller strategies
- Replace crude 30% remnant area estimate with L-shaped geometry
  calculation using actual grid extents and max utilization
- Move FillStrategyRegistry.SetEnabled to outer evaluation loop
  to avoid repeated enable/disable per remnant fill

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 23:08:55 -04:00
aj b6ee04f038 fix: use Part.Rotate() in PlateView to avoid mutating shared Programs
RotateSelectedParts was calling Program.Rotate() directly on shared
Program instances, bypassing Part's copy-on-write (EnsureOwnedProgram).
Parts created via CloneAtOffset share the same Program, so rotating one
part would rotate all parts with the same reference.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 23:08:47 -04:00
aj 8ffdacd6c0 refactor: replace NestPhase switch statements with attribute-based extensions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 19:49:44 -04:00
aj ccd402c50f refactor: simplify NestProgress with computed properties and ProgressReport struct
Replace stored property setters (BestPartCount, BestDensity, NestedWidth,
NestedLength, NestedArea) with computed properties that derive values from
BestParts, with a lazy cache invalidated on setter. Add internal
ProgressReport struct to replace the 7-parameter ReportProgress signature.
Update all 13 callsites and AccumulatingProgress. Delete FormatPhaseName
in favor of NestPhase.ShortName() extension.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 19:44:45 -04:00
aj b1e872577c feat: add Description/ShortName attributes to NestPhase with extension methods
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 19:38:54 -04:00
aj 9903478d3e refactor: simplify BestCombination.FindFrom2 and add tests
Remove redundant early-return branches and unify loop body — Floor(remaining/length2) already returns 0 when remaining < length2, so both branches collapse into one. 14 tests cover all edge cases.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 17:07:43 -04:00
24 changed files with 770 additions and 275 deletions
+27 -7
View File
@@ -66,8 +66,15 @@ namespace OpenNest
if (item.Quantity > 0 && best.Count > item.Quantity) if (item.Quantity > 0 && best.Count > item.Quantity)
best = ShrinkFiller.TrimToCount(best, item.Quantity, ShrinkAxis.Width); best = ShrinkFiller.TrimToCount(best, item.Quantity, ShrinkAxis.Width);
ReportProgress(progress, WinnerPhase, PlateNumber, best, workArea, BuildProgressSummary(), ReportProgress(progress, new ProgressReport
isOverallBest: true); {
Phase = WinnerPhase,
PlateNumber = PlateNumber,
Parts = best,
WorkArea = workArea,
Description = BuildProgressSummary(),
IsOverallBest = true,
});
return best; 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}"); 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(), ReportProgress(progress, new ProgressReport
isOverallBest: true); {
Phase = NestPhase.Linear,
PlateNumber = PlateNumber,
Parts = best,
WorkArea = workArea,
Description = BuildProgressSummary(),
IsOverallBest = true,
});
return best ?? new List<Part>(); return best ?? new List<Part>();
} }
@@ -151,9 +165,15 @@ namespace OpenNest
if (context.CurrentBest != null && context.CurrentBest.Count > 0) if (context.CurrentBest != null && context.CurrentBest.Count > 0)
{ {
ReportProgress(context.Progress, context.WinnerPhase, PlateNumber, ReportProgress(context.Progress, new ProgressReport
context.CurrentBest, context.WorkArea, BuildProgressSummary(), {
isOverallBest: true); Phase = context.WinnerPhase,
PlateNumber = PlateNumber,
Parts = context.CurrentBest,
WorkArea = context.WorkArea,
Description = BuildProgressSummary(),
IsOverallBest = true,
});
} }
} }
} }
@@ -26,7 +26,6 @@ namespace OpenNest.Engine.Fill
combined.AddRange(previousParts); combined.AddRange(previousParts);
combined.AddRange(value.BestParts); combined.AddRange(value.BestParts);
value.BestParts = combined; value.BestParts = combined;
value.BestPartCount = combined.Count;
} }
inner.Report(value); inner.Report(value);
+15 -59
View File
@@ -7,74 +7,30 @@ namespace OpenNest
public static bool FindFrom2(double length1, double length2, double overallLength, out int count1, out int count2) public static bool FindFrom2(double length1, double length2, double overallLength, out int count1, out int count2)
{ {
overallLength += Tolerance.Epsilon; overallLength += Tolerance.Epsilon;
count1 = 0;
if (length1 > overallLength)
{
if (length2 > overallLength)
{
count1 = 0;
count2 = 0;
return false;
}
count1 = 0;
count2 = (int)System.Math.Floor(overallLength / length2);
return true;
}
if (length2 > overallLength)
{
count1 = (int)System.Math.Floor(overallLength / length1);
count2 = 0;
return true;
}
var maxCountLength1 = (int)System.Math.Floor(overallLength / length1);
count1 = maxCountLength1;
count2 = 0; count2 = 0;
var remnant = overallLength - maxCountLength1 * length1; var maxCount1 = (int)System.Math.Floor(overallLength / length1);
var bestRemnant = overallLength + 1;
if (remnant.IsEqualTo(0)) for (var c1 = 0; c1 <= maxCount1; c1++)
return true;
for (int countLength1 = 0; countLength1 <= maxCountLength1; ++countLength1)
{ {
var remnant1 = overallLength - countLength1 * length1; var remaining = overallLength - c1 * length1;
var c2 = (int)System.Math.Floor(remaining / length2);
var remnant = remaining - c2 * length2;
if (remnant1 >= length2) if (!(remnant < bestRemnant))
{ continue;
var countLength2 = (int)System.Math.Floor(remnant1 / length2);
var remnant2 = remnant1 - length2 * countLength2;
if (!(remnant2 < remnant)) count1 = c1;
continue; count2 = c2;
bestRemnant = remnant;
count1 = countLength1; if (remnant.IsEqualTo(0))
count2 = countLength2; break;
if (remnant2.IsEqualTo(0))
break;
remnant = remnant2;
}
else
{
if (!(remnant1 < remnant))
continue;
count1 = countLength1;
count2 = 0;
if (remnant1.IsEqualTo(0))
break;
remnant = remnant1;
}
} }
return true; return count1 > 0 || count2 > 0;
} }
} }
} }
+24 -6
View File
@@ -36,18 +36,36 @@ namespace OpenNest.Engine.Fill
if (column.Count == 0) if (column.Count == 0)
return new List<Part>(); return new List<Part>();
NestEngineBase.ReportProgress(progress, NestPhase.Extents, plateNumber, NestEngineBase.ReportProgress(progress, new ProgressReport
column, workArea, $"Extents: initial column {column.Count} parts"); {
Phase = NestPhase.Extents,
PlateNumber = plateNumber,
Parts = column,
WorkArea = workArea,
Description = $"Extents: initial column {column.Count} parts",
});
var adjusted = AdjustColumn(pair.Value, column, token); var adjusted = AdjustColumn(pair.Value, column, token);
NestEngineBase.ReportProgress(progress, NestPhase.Extents, plateNumber, NestEngineBase.ReportProgress(progress, new ProgressReport
adjusted, workArea, $"Extents: adjusted column {adjusted.Count} parts"); {
Phase = NestPhase.Extents,
PlateNumber = plateNumber,
Parts = adjusted,
WorkArea = workArea,
Description = $"Extents: adjusted column {adjusted.Count} parts",
});
var result = RepeatColumns(adjusted, token); var result = RepeatColumns(adjusted, token);
NestEngineBase.ReportProgress(progress, NestPhase.Extents, plateNumber, NestEngineBase.ReportProgress(progress, new ProgressReport
result, workArea, $"Extents: {result.Count} parts total"); {
Phase = NestPhase.Extents,
PlateNumber = plateNumber,
Parts = result,
WorkArea = workArea,
Description = $"Extents: {result.Count} parts total",
});
return result; return result;
} }
+75
View File
@@ -0,0 +1,75 @@
using System;
using System.Collections.Concurrent;
using OpenNest.Geometry;
namespace OpenNest.Engine.Fill;
/// <summary>
/// Tracks evaluated grid configurations so duplicate pattern/direction/workArea
/// combinations can be skipped across fill strategies.
/// </summary>
public class GridDedup
{
public const string SharedStateKey = "GridDedup";
private readonly ConcurrentDictionary<GridKey, byte> _seen = new();
/// <summary>
/// Returns true if this configuration has NOT been seen before (i.e., should be evaluated).
/// Returns false if it's a duplicate.
/// </summary>
public bool TryAdd(Box patternBox, Box workArea, NestDirection dir)
{
var key = new GridKey(patternBox, workArea, dir);
return _seen.TryAdd(key, 0);
}
public int Count => _seen.Count;
/// <summary>
/// Gets or creates a GridDedup from FillContext.SharedState.
/// </summary>
public static GridDedup GetOrCreate(System.Collections.Generic.Dictionary<string, object> sharedState)
{
if (sharedState.TryGetValue(SharedStateKey, out var existing))
return (GridDedup)existing;
var dedup = new GridDedup();
sharedState[SharedStateKey] = dedup;
return dedup;
}
private readonly struct GridKey : IEquatable<GridKey>
{
private readonly int _patternW, _patternL, _workW, _workL, _dir;
public GridKey(Box patternBox, Box workArea, NestDirection dir)
{
_patternW = (int)System.Math.Round(patternBox.Width * 10);
_patternL = (int)System.Math.Round(patternBox.Length * 10);
_workW = (int)System.Math.Round(workArea.Width * 10);
_workL = (int)System.Math.Round(workArea.Length * 10);
_dir = (int)dir;
}
public bool Equals(GridKey other) =>
_patternW == other._patternW && _patternL == other._patternL &&
_workW == other._workW && _workL == other._workL &&
_dir == other._dir;
public override bool Equals(object obj) => obj is GridKey other && Equals(other);
public override int GetHashCode()
{
unchecked
{
var hash = _patternW;
hash = hash * 397 ^ _patternL;
hash = hash * 397 ^ _workW;
hash = hash * 397 ^ _workL;
hash = hash * 397 ^ _dir;
return hash;
}
}
}
}
+14 -4
View File
@@ -31,7 +31,8 @@ namespace OpenNest.Engine.Fill
double spacing, double spacing,
CancellationToken token = default, CancellationToken token = default,
IProgress<NestProgress> progress = null, IProgress<NestProgress> progress = null,
int plateNumber = 0) int plateNumber = 0,
Func<NestItem, Box, List<Part>> widthFillFunc = null)
{ {
if (items == null || items.Count == 0) if (items == null || items.Count == 0)
return new IterativeShrinkResult(); return new IterativeShrinkResult();
@@ -72,6 +73,8 @@ namespace OpenNest.Engine.Fill
// include them in progress reports. // include them in progress reports.
var placedSoFar = new List<Part>(); var placedSoFar = new List<Part>();
var wFillFunc = widthFillFunc ?? fillFunc;
Func<NestItem, Box, List<Part>> shrinkWrapper = (ni, box) => Func<NestItem, Box, List<Part>> shrinkWrapper = (ni, box) =>
{ {
var target = ni.Quantity > 0 ? ni.Quantity : 0; var target = ni.Quantity > 0 ? ni.Quantity : 0;
@@ -84,7 +87,7 @@ namespace OpenNest.Engine.Fill
Parallel.Invoke( Parallel.Invoke(
() => heightResult = ShrinkFiller.Shrink(fillFunc, ni, box, spacing, ShrinkAxis.Height, token, () => heightResult = ShrinkFiller.Shrink(fillFunc, ni, box, spacing, ShrinkAxis.Height, token,
targetCount: target, progress: progress, plateNumber: plateNumber, placedParts: placedSoFar), targetCount: target, progress: progress, plateNumber: plateNumber, placedParts: placedSoFar),
() => widthResult = ShrinkFiller.Shrink(fillFunc, ni, box, spacing, ShrinkAxis.Width, token, () => widthResult = ShrinkFiller.Shrink(wFillFunc, ni, box, spacing, ShrinkAxis.Width, token,
targetCount: target, progress: progress, plateNumber: plateNumber, placedParts: placedSoFar) targetCount: target, progress: progress, plateNumber: plateNumber, placedParts: placedSoFar)
); );
@@ -108,8 +111,15 @@ namespace OpenNest.Engine.Fill
var allParts = new List<Part>(placedSoFar.Count + best.Count); var allParts = new List<Part>(placedSoFar.Count + best.Count);
allParts.AddRange(placedSoFar); allParts.AddRange(placedSoFar);
allParts.AddRange(best); allParts.AddRange(best);
NestEngineBase.ReportProgress(progress, NestPhase.Custom, plateNumber, NestEngineBase.ReportProgress(progress, new ProgressReport
allParts, box, $"Shrink: {best.Count} parts placed", isOverallBest: true); {
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. // Accumulate for the next item's progress reports.
+101 -45
View File
@@ -7,6 +7,7 @@ using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks;
using OpenNest.Engine; using OpenNest.Engine;
namespace OpenNest.Engine.Fill namespace OpenNest.Engine.Fill
@@ -32,13 +33,15 @@ namespace OpenNest.Engine.Fill
private readonly Size plateSize; private readonly Size plateSize;
private readonly double partSpacing; private readonly double partSpacing;
private readonly IFillComparer comparer; private readonly IFillComparer comparer;
private readonly GridDedup dedup;
public PairFiller(Plate plate, IFillComparer comparer = null) public PairFiller(Plate plate, IFillComparer comparer = null, GridDedup dedup = null)
{ {
this.plate = plate; this.plate = plate;
this.plateSize = plate.Size; this.plateSize = plate.Size;
this.partSpacing = plate.PartSpacing; this.partSpacing = plate.PartSpacing;
this.comparer = comparer ?? new DefaultFillComparer(); this.comparer = comparer ?? new DefaultFillComparer();
this.dedup = dedup ?? new GridDedup();
} }
public PairFillResult Fill(NestItem item, Box workArea, public PairFillResult Fill(NestItem item, Box workArea,
@@ -68,32 +71,60 @@ namespace OpenNest.Engine.Fill
List<Part> best = null; List<Part> best = null;
var sinceImproved = 0; var sinceImproved = 0;
var effectiveWorkArea = workArea; var effectiveWorkArea = workArea;
var batchSize = System.Math.Max(2, Environment.ProcessorCount);
var maxUtilization = candidates.Count > 0 ? candidates.Max(c => c.Utilization) : 1.0;
var partBox = drawing.Program.BoundingBox();
var partArea = System.Math.Max(partBox.Width * partBox.Length, 1);
FillStrategyRegistry.SetEnabled("Pairs", "RectBestFit", "Extents", "Linear");
try try
{ {
for (var i = 0; i < candidates.Count; i++) for (var batchStart = 0; batchStart < candidates.Count; batchStart += batchSize)
{ {
token.ThrowIfCancellationRequested(); token.ThrowIfCancellationRequested();
var filled = EvaluateCandidate(candidates[i], drawing, effectiveWorkArea, token); var batchEnd = System.Math.Min(batchStart + batchSize, candidates.Count);
var batchCount = batchEnd - batchStart;
var batchWorkArea = effectiveWorkArea;
var minCountToBeat = best?.Count ?? 0;
if (comparer.IsBetter(filled, best, effectiveWorkArea)) var results = new List<Part>[batchCount];
Parallel.For(0, batchCount,
new ParallelOptions { CancellationToken = token },
j =>
{
results[j] = EvaluateCandidate(
candidates[batchStart + j], drawing, batchWorkArea,
minCountToBeat, maxUtilization, partArea, token);
});
for (var j = 0; j < batchCount; j++)
{ {
best = filled; if (comparer.IsBetter(results[j], best, effectiveWorkArea))
sinceImproved = 0; {
effectiveWorkArea = TryReduceWorkArea(filled, targetCount, workArea, effectiveWorkArea); best = results[j];
} sinceImproved = 0;
else effectiveWorkArea = TryReduceWorkArea(best, targetCount, workArea, effectiveWorkArea);
{ }
sinceImproved++; else
{
sinceImproved++;
}
NestEngineBase.ReportProgress(progress, new ProgressReport
{
Phase = NestPhase.Pairs,
PlateNumber = plateNumber,
Parts = best,
WorkArea = workArea,
Description = $"Pairs: {batchStart + j + 1}/{candidates.Count} candidates, best = {best?.Count ?? 0} parts",
});
} }
NestEngineBase.ReportProgress(progress, NestPhase.Pairs, plateNumber, best, workArea, if (batchEnd >= EarlyExitMinTried && sinceImproved >= EarlyExitStaleLimit)
$"Pairs: {i + 1}/{candidates.Count} candidates, best = {best?.Count ?? 0} parts");
if (i + 1 >= EarlyExitMinTried && sinceImproved >= EarlyExitStaleLimit)
{ {
Debug.WriteLine($"[PairFiller] Early exit at {i + 1}/{candidates.Count} — no improvement in last {sinceImproved} candidates"); Debug.WriteLine($"[PairFiller] Early exit at {batchEnd}/{candidates.Count} — no improvement in last {sinceImproved} candidates");
break; break;
} }
} }
@@ -102,6 +133,10 @@ namespace OpenNest.Engine.Fill
{ {
Debug.WriteLine("[PairFiller] Cancelled mid-phase, using results so far"); Debug.WriteLine("[PairFiller] Cancelled mid-phase, using results so far");
} }
finally
{
FillStrategyRegistry.SetEnabled(null);
}
Debug.WriteLine($"[PairFiller] Best pair result: {best?.Count ?? 0} parts"); Debug.WriteLine($"[PairFiller] Best pair result: {best?.Count ?? 0} parts");
return best ?? new List<Part>(); return best ?? new List<Part>();
@@ -145,7 +180,8 @@ namespace OpenNest.Engine.Fill
} }
private List<Part> EvaluateCandidate(BestFitResult candidate, Drawing drawing, private List<Part> EvaluateCandidate(BestFitResult candidate, Drawing drawing,
Box workArea, CancellationToken token) Box workArea, int minCountToBeat, double maxUtilization, double partArea,
CancellationToken token)
{ {
var pairParts = candidate.BuildParts(drawing); var pairParts = candidate.BuildParts(drawing);
var angles = BuildTilingAngles(candidate); var angles = BuildTilingAngles(candidate);
@@ -162,6 +198,9 @@ namespace OpenNest.Engine.Fill
var engine = new FillLinear(workArea, partSpacing); var engine = new FillLinear(workArea, partSpacing);
foreach (var dir in new[] { NestDirection.Horizontal, NestDirection.Vertical }) foreach (var dir in new[] { NestDirection.Horizontal, NestDirection.Vertical })
{ {
if (!dedup.TryAdd(pattern.BoundingBox, workArea, dir))
continue;
var gridParts = engine.Fill(pattern, dir); var gridParts = engine.Fill(pattern, dir);
if (gridParts != null && gridParts.Count > 0) if (gridParts != null && gridParts.Count > 0)
grids.Add((gridParts, dir)); grids.Add((gridParts, dir));
@@ -174,17 +213,34 @@ namespace OpenNest.Engine.Fill
// Sort by count descending so we try the best grids first // Sort by count descending so we try the best grids first
grids.Sort((a, b) => b.Parts.Count.CompareTo(a.Parts.Count)); grids.Sort((a, b) => b.Parts.Count.CompareTo(a.Parts.Count));
// Early abort: if the best grid + optimistic remnant can't beat the global best, skip Phase 2
if (minCountToBeat > 0)
{
var topCount = grids[0].Parts.Count;
var optimisticRemnant = EstimateRemnantUpperBound(
grids[0].Parts, workArea, maxUtilization, partArea);
if (topCount + optimisticRemnant <= minCountToBeat)
{
Debug.WriteLine($"[PairFiller] Skipping candidate: grid {topCount} + estimate {optimisticRemnant} <= best {minCountToBeat}");
return null;
}
}
// Phase 2: try remnant for each grid, skip if grid is too far behind // Phase 2: try remnant for each grid, skip if grid is too far behind
List<Part> best = null; List<Part> best = null;
var maxRemnantEstimate = EstimateMaxRemnantParts(drawing, workArea);
foreach (var (gridParts, dir) in grids) foreach (var (gridParts, dir) in grids)
{ {
token.ThrowIfCancellationRequested(); token.ThrowIfCancellationRequested();
// If this grid + max possible remnant can't beat current best, skip // If this grid + max possible remnant can't beat current best, skip
if (best != null && gridParts.Count + maxRemnantEstimate <= best.Count) if (best != null)
break; // sorted descending, so remaining are even smaller {
var remnantBound = EstimateRemnantUpperBound(
gridParts, workArea, maxUtilization, partArea);
if (gridParts.Count + remnantBound <= best.Count)
break; // sorted descending, so remaining are even smaller
}
var remnantParts = FillRemnant(gridParts, drawing, workArea, token); var remnantParts = FillRemnant(gridParts, drawing, workArea, token);
List<Part> total; List<Part> total;
@@ -206,12 +262,20 @@ namespace OpenNest.Engine.Fill
return best; return best;
} }
private static int EstimateMaxRemnantParts(Drawing drawing, Box workArea) private int EstimateRemnantUpperBound(List<Part> gridParts, Box workArea,
double maxUtilization, double partArea)
{ {
var partBox = drawing.Program.BoundingBox(); var gridBox = ((IEnumerable<IBoundable>)gridParts).GetBoundingBox();
var partArea = System.Math.Max(partBox.Width * partBox.Length, 1);
var remnantArea = workArea.Area() * 0.3; // remnant is at most ~30% of work area // L-shaped remnant: top strip (full width) + right strip (grid height only)
return (int)(remnantArea / partArea) + 1; var topHeight = System.Math.Max(0, workArea.Top - gridBox.Top);
var rightWidth = System.Math.Max(0, workArea.Right - gridBox.Right);
var topArea = workArea.Width * topHeight;
var rightArea = rightWidth * System.Math.Min(gridBox.Top - workArea.Y, workArea.Length);
var remnantArea = topArea + rightArea;
return (int)(remnantArea * maxUtilization / partArea) + 1;
} }
private List<Part> FillRemnant(List<Part> gridParts, Drawing drawing, private List<Part> FillRemnant(List<Part> gridParts, Drawing drawing,
@@ -257,28 +321,20 @@ namespace OpenNest.Engine.Fill
return cachedResult; return cachedResult;
} }
FillStrategyRegistry.SetEnabled("Pairs", "RectBestFit", "Extents", "Linear"); var remnantEngine = NestEngineRegistry.Create(plate);
try var item = new NestItem { Drawing = drawing };
var parts = remnantEngine.Fill(item, remnantBox, null, token);
Debug.WriteLine($"[PairFiller] Remnant: {parts?.Count ?? 0} parts in " +
$"{remnantBox.Width:F2}x{remnantBox.Length:F2}");
if (parts != null && parts.Count > 0)
{ {
var remnantEngine = NestEngineRegistry.Create(plate); FillResultCache.Store(drawing, remnantBox, partSpacing, parts);
var item = new NestItem { Drawing = drawing }; return parts;
var parts = remnantEngine.Fill(item, remnantBox, null, token);
Debug.WriteLine($"[PairFiller] Remnant: {parts?.Count ?? 0} parts in " +
$"{remnantBox.Width:F2}x{remnantBox.Length:F2}");
if (parts != null && parts.Count > 0)
{
FillResultCache.Store(drawing, remnantBox, partSpacing, parts);
return parts;
}
return null;
}
finally
{
FillStrategyRegistry.SetEnabled(null);
} }
return null;
} }
private static List<double> BuildTilingAngles(BestFitResult candidate) private static List<double> BuildTilingAngles(BestFitResult candidate)
+8 -2
View File
@@ -79,8 +79,14 @@ namespace OpenNest.Engine.Fill
var desc = $"Shrink {axis}: {bestParts.Count} parts, dim={dim:F1}"; var desc = $"Shrink {axis}: {bestParts.Count} parts, dim={dim:F1}";
NestEngineBase.ReportProgress(progress, NestPhase.Custom, plateNumber, NestEngineBase.ReportProgress(progress, new ProgressReport
allParts, workArea, desc); {
Phase = NestPhase.Custom,
PlateNumber = plateNumber,
Parts = allParts,
WorkArea = workArea,
Description = desc,
});
} }
/// <summary> /// <summary>
+14 -3
View File
@@ -20,6 +20,7 @@ public class StripeFiller
private readonly FillContext _context; private readonly FillContext _context;
private readonly NestDirection _primaryAxis; private readonly NestDirection _primaryAxis;
private readonly IFillComparer _comparer; private readonly IFillComparer _comparer;
private readonly GridDedup _dedup;
/// <summary> /// <summary>
/// When true, only complete stripes are placed — no partial rows/columns. /// When true, only complete stripes are placed — no partial rows/columns.
@@ -38,6 +39,7 @@ public class StripeFiller
_context = context; _context = context;
_primaryAxis = primaryAxis; _primaryAxis = primaryAxis;
_comparer = context.Policy?.Comparer ?? new DefaultFillComparer(); _comparer = context.Policy?.Comparer ?? new DefaultFillComparer();
_dedup = GridDedup.GetOrCreate(context.SharedState);
} }
public List<Part> Fill() public List<Part> Fill()
@@ -93,9 +95,14 @@ public class StripeFiller
} }
} }
NestEngineBase.ReportProgress(_context.Progress, NestPhase.Custom, NestEngineBase.ReportProgress(_context.Progress, new ProgressReport
_context.PlateNumber, bestParts, workArea, {
$"{strategyName}: {i + 1}/{bestFits.Count} pairs, best = {bestParts?.Count ?? 0} parts"); 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<Part>(); return bestParts ?? new List<Part>();
@@ -110,6 +117,10 @@ public class StripeFiller
var rotatedPattern = FillHelpers.BuildRotatedPattern(pairParts, angle); var rotatedPattern = FillHelpers.BuildRotatedPattern(pairParts, angle);
var perpDim = GetDimension(rotatedPattern.BoundingBox, perpAxis); var perpDim = GetDimension(rotatedPattern.BoundingBox, perpAxis);
var stripeBox = MakeStripeBox(workArea, perpDim, primaryAxis); var stripeBox = MakeStripeBox(workArea, perpDim, primaryAxis);
if (!_dedup.TryAdd(rotatedPattern.BoundingBox, workArea, primaryAxis))
return null;
var stripeEngine = new FillLinear(stripeBox, spacing); var stripeEngine = new FillLinear(stripeBox, spacing);
var stripeParts = stripeEngine.Fill(rotatedPattern, primaryAxis); var stripeParts = stripeEngine.Fill(rotatedPattern, primaryAxis);
+12 -53
View File
@@ -210,55 +210,26 @@ namespace OpenNest
// --- Protected utilities --- // --- Protected utilities ---
internal static void ReportProgress( internal static void ReportProgress(
IProgress<NestProgress> progress, IProgress<NestProgress> progress, ProgressReport report)
NestPhase phase,
int plateNumber,
List<Part> best,
Box workArea,
string description,
bool isOverallBest = false)
{ {
if (progress == null || best == null || best.Count == 0) if (progress == null || report.Parts == null || report.Parts.Count == 0)
return; return;
var score = FillScore.Compute(best, workArea); var clonedParts = new List<Part>(report.Parts.Count);
var clonedParts = new List<Part>(best.Count); foreach (var part in report.Parts)
var totalPartArea = 0.0;
foreach (var part in best)
{
clonedParts.Add((Part)part.Clone()); clonedParts.Add((Part)part.Clone());
totalPartArea += part.BaseDrawing.Area;
}
var bounds = best.GetBoundingBox(); Debug.WriteLine($"[Progress] Phase={report.Phase}, Plate={report.PlateNumber}, " +
$"Parts={clonedParts.Count} | {report.Description}");
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 { }
progress.Report(new NestProgress progress.Report(new NestProgress
{ {
Phase = phase, Phase = report.Phase,
PlateNumber = plateNumber, PlateNumber = report.PlateNumber,
BestPartCount = score.Count,
BestDensity = score.Density,
NestedWidth = bounds.Width,
NestedLength = bounds.Length,
NestedArea = totalPartArea,
BestParts = clonedParts, BestParts = clonedParts,
Description = description, Description = report.Description,
ActiveWorkArea = workArea, ActiveWorkArea = report.WorkArea,
IsOverallBest = isOverallBest, IsOverallBest = report.IsOverallBest,
}); });
} }
@@ -270,7 +241,7 @@ namespace OpenNest
var parts = new List<string>(PhaseResults.Count); var parts = new List<string>(PhaseResults.Count);
foreach (var r in PhaseResults) foreach (var r in PhaseResults)
parts.Add($"{FormatPhaseName(r.Phase)}: {r.PartCount}"); parts.Add($"{r.Phase.ShortName()}: {r.PartCount}");
return string.Join(" | ", parts); return string.Join(" | ", parts);
} }
@@ -323,17 +294,5 @@ namespace OpenNest
return false; 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();
}
}
} }
} }
+123 -12
View File
@@ -1,16 +1,52 @@
using OpenNest.Geometry; using OpenNest.Geometry;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel;
using System.Reflection;
namespace OpenNest namespace OpenNest
{ {
[AttributeUsage(AttributeTargets.Field)]
internal class ShortNameAttribute(string name) : Attribute
{
public string Name { get; } = name;
}
public enum NestPhase public enum NestPhase
{ {
Linear, [Description("Trying rotations..."), ShortName("Linear")] Linear,
RectBestFit, [Description("Trying best fit..."), ShortName("BestFit")] RectBestFit,
Pairs, [Description("Trying pairs..."), ShortName("Pairs")] Pairs,
Nfp, [Description("Trying NFP..."), ShortName("NFP")] Nfp,
Extents, [Description("Trying extents..."), ShortName("Extents")] Extents,
Custom [Description("Custom"), ShortName("Custom")] Custom
}
public static class NestPhaseExtensions
{
private static readonly ConcurrentDictionary<NestPhase, string> DisplayNames = new();
private static readonly ConcurrentDictionary<NestPhase, string> ShortNames = new();
public static string DisplayName(this NestPhase phase)
{
return DisplayNames.GetOrAdd(phase, p =>
{
var field = typeof(NestPhase).GetField(p.ToString());
var attr = field?.GetCustomAttribute<DescriptionAttribute>();
return attr?.Description ?? p.ToString();
});
}
public static string ShortName(this NestPhase phase)
{
return ShortNames.GetOrAdd(phase, p =>
{
var field = typeof(NestPhase).GetField(p.ToString());
var attr = field?.GetCustomAttribute<ShortNameAttribute>();
return attr?.Name ?? p.ToString();
});
}
} }
public class PhaseResult public class PhaseResult
@@ -34,18 +70,93 @@ namespace OpenNest
public int PartCount { get; set; } public int PartCount { get; set; }
} }
internal readonly struct ProgressReport
{
public NestPhase Phase { get; init; }
public int PlateNumber { get; init; }
public List<Part> Parts { get; init; }
public Box WorkArea { get; init; }
public string Description { get; init; }
public bool IsOverallBest { get; init; }
}
public class NestProgress public class NestProgress
{ {
public NestPhase Phase { get; set; } public NestPhase Phase { get; set; }
public int PlateNumber { get; set; } public int PlateNumber { get; set; }
public int BestPartCount { get; set; }
public double BestDensity { get; set; } private List<Part> bestParts;
public double NestedWidth { get; set; } public List<Part> BestParts
public double NestedLength { get; set; } {
public double NestedArea { get; set; } get => bestParts;
public List<Part> BestParts { get; set; } set { bestParts = value; cachedParts = null; }
}
public string Description { get; set; } public string Description { get; set; }
public Box ActiveWorkArea { get; set; } public Box ActiveWorkArea { get; set; }
public bool IsOverallBest { get; set; } public bool IsOverallBest { get; set; }
public int BestPartCount => BestParts?.Count ?? 0;
private List<Part> 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;
}
}
} }
} }
+9 -2
View File
@@ -74,8 +74,15 @@ namespace OpenNest.Engine.Nfp
Debug.WriteLine($"[AutoNest] Result: {parts.Count} parts placed, {result.Iterations} SA iterations"); Debug.WriteLine($"[AutoNest] Result: {parts.Count} parts placed, {result.Iterations} SA iterations");
NestEngineBase.ReportProgress(progress, NestPhase.Nfp, 0, parts, workArea, NestEngineBase.ReportProgress(progress, new ProgressReport
$"NFP: {parts.Count} parts, {result.Iterations} iterations", isOverallBest: true); {
Phase = NestPhase.Nfp,
PlateNumber = 0,
Parts = parts,
WorkArea = workArea,
Description = $"NFP: {parts.Count} parts, {result.Iterations} iterations",
IsOverallBest = true,
});
return parts; return parts;
} }
+9 -2
View File
@@ -277,8 +277,15 @@ namespace OpenNest.Engine.Nfp
private static void ReportBest(IProgress<NestProgress> progress, List<Part> parts, private static void ReportBest(IProgress<NestProgress> progress, List<Part> parts,
Box workArea, string description) Box workArea, string description)
{ {
NestEngineBase.ReportProgress(progress, NestPhase.Nfp, 0, parts, workArea, NestEngineBase.ReportProgress(progress, new ProgressReport
description, isOverallBest: true); {
Phase = NestPhase.Nfp,
PlateNumber = 0,
Parts = parts,
WorkArea = workArea,
Description = description,
IsOverallBest = true,
});
} }
} }
} }
@@ -47,9 +47,14 @@ namespace OpenNest.Engine.Strategies
best = result; best = result;
} }
NestEngineBase.ReportProgress(context.Progress, NestPhase.Linear, NestEngineBase.ReportProgress(context.Progress, new ProgressReport
context.PlateNumber, best, workArea, {
$"Linear: {ai + 1}/{angles.Count} angles, {angleDeg:F0}° best = {best?.Count ?? 0} parts"); 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<Part>(); return best ?? new List<Part>();
@@ -12,7 +12,8 @@ namespace OpenNest.Engine.Strategies
public List<Part> Fill(FillContext context) public List<Part> Fill(FillContext context)
{ {
var comparer = context.Policy?.Comparer; var comparer = context.Policy?.Comparer;
var filler = new PairFiller(context.Plate, comparer); var dedup = GridDedup.GetOrCreate(context.SharedState);
var filler = new PairFiller(context.Plate, comparer, dedup);
var result = filler.Fill(context.Item, context.WorkArea, var result = filler.Fill(context.Item, context.WorkArea,
context.PlateNumber, context.Token, context.Progress); context.PlateNumber, context.Token, context.Progress);
+12 -6
View File
@@ -77,17 +77,23 @@ namespace OpenNest
// Phase 1: Iterative shrink-fill for multi-quantity items. // Phase 1: Iterative shrink-fill for multi-quantity items.
if (fillItems.Count > 0) if (fillItems.Count > 0)
{ {
// Pass progress through so the UI shows intermediate results // Use direction-specific engines: height shrink benefits from
// during the initial BestFitCache computation and fill phases. // minimizing Y-extent, width shrink from minimizing X-extent.
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) => Func<NestItem, Box, List<Part>> heightFillFunc = (ni, b) =>
{ {
var inner = new DefaultNestEngine(Plate); var inner = new HorizontalRemnantEngine(Plate);
return inner.Fill(ni, b, progress, token);
};
Func<NestItem, Box, List<Part>> widthFillFunc = (ni, b) =>
{
var inner = new VerticalRemnantEngine(Plate);
return inner.Fill(ni, b, progress, token); return inner.Fill(ni, b, progress, token);
}; };
var shrinkResult = IterativeShrinkFiller.Fill( var shrinkResult = IterativeShrinkFiller.Fill(
fillItems, workArea, fillFunc, Plate.PartSpacing, token, fillItems, workArea, heightFillFunc, Plate.PartSpacing, token,
progress, PlateNumber); progress, PlateNumber, widthFillFunc);
allParts.AddRange(shrinkResult.Parts); allParts.AddRange(shrinkResult.Parts);
+2 -2
View File
@@ -18,7 +18,7 @@ public class AccumulatingProgressTests
var accumulating = new AccumulatingProgress(inner, previous); var accumulating = new AccumulatingProgress(inner, previous);
var newParts = new List<Part> { TestHelpers.MakePartAt(20, 0, 10) }; var newParts = new List<Part> { TestHelpers.MakePartAt(20, 0, 10) };
accumulating.Report(new NestProgress { BestParts = newParts, BestPartCount = 1 }); accumulating.Report(new NestProgress { BestParts = newParts });
Assert.NotNull(inner.Last); Assert.NotNull(inner.Last);
Assert.Equal(2, inner.Last.BestParts.Count); Assert.Equal(2, inner.Last.BestParts.Count);
@@ -32,7 +32,7 @@ public class AccumulatingProgressTests
var accumulating = new AccumulatingProgress(inner, new List<Part>()); var accumulating = new AccumulatingProgress(inner, new List<Part>());
var newParts = new List<Part> { TestHelpers.MakePartAt(0, 0, 10) }; var newParts = new List<Part> { TestHelpers.MakePartAt(0, 0, 10) };
accumulating.Report(new NestProgress { BestParts = newParts, BestPartCount = 1 }); accumulating.Report(new NestProgress { BestParts = newParts });
Assert.NotNull(inner.Last); Assert.NotNull(inner.Last);
Assert.Single(inner.Last.BestParts); Assert.Single(inner.Last.BestParts);
+150
View File
@@ -0,0 +1,150 @@
namespace OpenNest.Tests;
public class BestCombinationTests
{
[Fact]
public void BothFit_FindsZeroRemnant()
{
// 100 = 0*30 + 5*20 (algorithm iterates from countLength1=0, finds zero remnant first)
var result = BestCombination.FindFrom2(30, 20, 100, out var c1, out var c2);
Assert.True(result);
Assert.Equal(0.0, 100.0 - (c1 * 30.0 + c2 * 20.0), 5);
}
[Fact]
public void OnlyLength1Fits_ReturnsMaxCount1()
{
var result = BestCombination.FindFrom2(10, 200, 50, out var c1, out var c2);
Assert.True(result);
Assert.Equal(5, c1);
Assert.Equal(0, c2);
}
[Fact]
public void OnlyLength2Fits_ReturnsMaxCount2()
{
var result = BestCombination.FindFrom2(200, 10, 50, out var c1, out var c2);
Assert.True(result);
Assert.Equal(0, c1);
Assert.Equal(5, c2);
}
[Fact]
public void NeitherFits_ReturnsFalse()
{
var result = BestCombination.FindFrom2(100, 200, 50, out var c1, out var c2);
Assert.False(result);
Assert.Equal(0, c1);
Assert.Equal(0, c2);
}
[Fact]
public void Length1FillsExactly_ZeroRemnant()
{
var result = BestCombination.FindFrom2(25, 10, 100, out var c1, out var c2);
Assert.True(result);
Assert.Equal(0.0, 100.0 - (c1 * 25.0 + c2 * 10.0), 5);
}
[Fact]
public void MixMinimizesRemnant()
{
// 7 and 3 into 20: best is 2*7 + 2*3 = 20 (zero remnant)
var result = BestCombination.FindFrom2(7, 3, 20, out var c1, out var c2);
Assert.True(result);
Assert.Equal(2, c1);
Assert.Equal(2, c2);
Assert.True(c1 * 7 + c2 * 3 <= 20);
}
[Fact]
public void PrefersLessRemnant_OverMoreOfLength1()
{
// 6 and 5 into 17:
// all length1: 2*6=12, remnant=5 -> actually 2*6+1*5=17 perfect
var result = BestCombination.FindFrom2(6, 5, 17, out var c1, out var c2);
Assert.True(result);
Assert.Equal(0.0, 17.0 - (c1 * 6.0 + c2 * 5.0), 5);
}
[Fact]
public void EqualLengths_FillsWithLength1()
{
var result = BestCombination.FindFrom2(10, 10, 50, out var c1, out var c2);
Assert.True(result);
Assert.Equal(5, c1 + c2);
}
[Fact]
public void SmallLengths_LargeOverall()
{
var result = BestCombination.FindFrom2(3, 7, 100, out var c1, out var c2);
Assert.True(result);
var used = c1 * 3.0 + c2 * 7.0;
Assert.True(used <= 100);
Assert.True(100 - used < 3); // remnant less than smallest piece
}
[Fact]
public void Length2IsBetter_SoleCandidate()
{
// length1=9, length2=5, overall=10:
// length1 alone: 1*9=9 remnant=1
// length2 alone: 2*5=10 remnant=0
var result = BestCombination.FindFrom2(9, 5, 10, out var c1, out var c2);
Assert.True(result);
Assert.Equal(0, c1);
Assert.Equal(2, c2);
}
[Fact]
public void FractionalLengths_WorkCorrectly()
{
var result = BestCombination.FindFrom2(2.5, 3.5, 12, out var c1, out var c2);
Assert.True(result);
var used = c1 * 2.5 + c2 * 3.5;
Assert.True(used <= 12.0 + 0.001);
}
[Fact]
public void OverallExactlyOneOfEach()
{
var result = BestCombination.FindFrom2(40, 60, 100, out var c1, out var c2);
Assert.True(result);
Assert.Equal(1, c1);
Assert.Equal(1, c2);
}
[Fact]
public void OverallSmallerThanEither_ReturnsFalse()
{
var result = BestCombination.FindFrom2(10, 20, 5, out var c1, out var c2);
Assert.False(result);
Assert.Equal(0, c1);
Assert.Equal(0, c2);
}
[Fact]
public void ZeroRemnant_StopsEarly()
{
// 4 and 6 into 24: 0*4+4*6=24 or 3*4+2*6=24 or 6*4+0*6=24
// Algorithm iterates from 0 length1 upward, finds zero remnant and breaks
var result = BestCombination.FindFrom2(4, 6, 24, out var c1, out var c2);
Assert.True(result);
Assert.Equal(0.0, 24.0 - (c1 * 4.0 + c2 * 6.0), 5);
}
}
@@ -0,0 +1,28 @@
namespace OpenNest.Tests;
public class NestPhaseExtensionsTests
{
[Theory]
[InlineData(NestPhase.Linear, "Trying rotations...")]
[InlineData(NestPhase.RectBestFit, "Trying best fit...")]
[InlineData(NestPhase.Pairs, "Trying pairs...")]
[InlineData(NestPhase.Nfp, "Trying NFP...")]
[InlineData(NestPhase.Extents, "Trying extents...")]
[InlineData(NestPhase.Custom, "Custom")]
public void DisplayName_ReturnsDescription(NestPhase phase, string expected)
{
Assert.Equal(expected, phase.DisplayName());
}
[Theory]
[InlineData(NestPhase.Linear, "Linear")]
[InlineData(NestPhase.RectBestFit, "BestFit")]
[InlineData(NestPhase.Pairs, "Pairs")]
[InlineData(NestPhase.Nfp, "NFP")]
[InlineData(NestPhase.Extents, "Extents")]
[InlineData(NestPhase.Custom, "Custom")]
public void ShortName_ReturnsShortLabel(NestPhase phase, string expected)
{
Assert.Equal(expected, phase.ShortName());
}
}
+100
View File
@@ -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<Part>
{
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<Part>
{
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<Part>
{
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<Part>
{
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<Part>
{
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<Part> { TestHelpers.MakePartAt(0, 0, 5) };
var parts2 = new List<Part>
{
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);
}
}
+1 -11
View File
@@ -59,16 +59,6 @@ namespace OpenNest.Controls
} }
} }
private static string GetDisplayName(NestPhase phase)
{
switch (phase)
{
case NestPhase.RectBestFit: return "BestFit";
case NestPhase.Nfp: return "NFP";
default: return phase.ToString();
}
}
protected override void OnPaint(PaintEventArgs e) protected override void OnPaint(PaintEventArgs e)
{ {
base.OnPaint(e); base.OnPaint(e);
@@ -134,7 +124,7 @@ namespace OpenNest.Controls
} }
// Label // Label
var label = GetDisplayName(phase); var label = phase.ShortName();
var font = isVisited || isActive ? BoldLabelFont : LabelFont; var font = isVisited || isActive ? BoldLabelFont : LabelFont;
var brush = isVisited || isActive ? activeTextBrush : pendingTextBrush; var brush = isVisited || isActive ? activeTextBrush : pendingTextBrush;
var labelSize = g.MeasureString(label, font); var labelSize = g.MeasureString(label, font);
+3 -10
View File
@@ -1098,23 +1098,16 @@ namespace OpenNest.Controls
var bounds = parts.GetBoundingBox(); var bounds = parts.GetBoundingBox();
var center = bounds.Center; var center = bounds.Center;
var anchor = bounds.Location; var anchor = bounds.Location;
var rotatedPrograms = new HashSet<Program>();
for (int i = 0; i < SelectedParts.Count; ++i) for (var i = 0; i < SelectedParts.Count; ++i)
{ {
var part = SelectedParts[i]; var part = SelectedParts[i];
var basePart = part.BasePart; part.BasePart.Rotate(angle, center);
if (rotatedPrograms.Add(basePart.Program))
basePart.Program.Rotate(angle);
part.Location = part.Location.Rotate(angle, center);
basePart.UpdateBounds();
} }
var diff = anchor - parts.GetBoundingBox().Location; var diff = anchor - parts.GetBoundingBox().Location;
for (int i = 0; i < SelectedParts.Count; ++i) for (var i = 0; i < SelectedParts.Count; ++i)
SelectedParts[i].Offset(diff); SelectedParts[i].Offset(diff);
} }
+32 -32
View File
@@ -85,13 +85,13 @@ namespace OpenNest.Forms
resultsTable.Controls.Add(nestedAreaLabel, 0, 2); resultsTable.Controls.Add(nestedAreaLabel, 0, 2);
resultsTable.Controls.Add(nestedAreaValue, 1, 2); resultsTable.Controls.Add(nestedAreaValue, 1, 2);
resultsTable.Dock = System.Windows.Forms.DockStyle.Top; resultsTable.Dock = System.Windows.Forms.DockStyle.Top;
resultsTable.Location = new System.Drawing.Point(14, 29); resultsTable.Location = new System.Drawing.Point(14, 33);
resultsTable.Name = "resultsTable"; resultsTable.Name = "resultsTable";
resultsTable.RowCount = 3; resultsTable.RowCount = 3;
resultsTable.RowStyles.Add(new System.Windows.Forms.RowStyle()); resultsTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
resultsTable.RowStyles.Add(new System.Windows.Forms.RowStyle()); resultsTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
resultsTable.RowStyles.Add(new System.Windows.Forms.RowStyle()); resultsTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
resultsTable.Size = new System.Drawing.Size(422, 57); resultsTable.Size = new System.Drawing.Size(422, 69);
resultsTable.TabIndex = 1; resultsTable.TabIndex = 1;
// //
// partsLabel // partsLabel
@@ -102,7 +102,7 @@ namespace OpenNest.Forms
partsLabel.Location = new System.Drawing.Point(0, 3); partsLabel.Location = new System.Drawing.Point(0, 3);
partsLabel.Margin = new System.Windows.Forms.Padding(0, 3, 5, 3); partsLabel.Margin = new System.Windows.Forms.Padding(0, 3, 5, 3);
partsLabel.Name = "partsLabel"; partsLabel.Name = "partsLabel";
partsLabel.Size = new System.Drawing.Size(36, 13); partsLabel.Size = new System.Drawing.Size(43, 17);
partsLabel.TabIndex = 0; partsLabel.TabIndex = 0;
partsLabel.Text = "Parts:"; partsLabel.Text = "Parts:";
// //
@@ -110,10 +110,10 @@ namespace OpenNest.Forms
// //
partsValue.AutoSize = true; partsValue.AutoSize = true;
partsValue.Font = new System.Drawing.Font("Consolas", 9.75F); partsValue.Font = new System.Drawing.Font("Consolas", 9.75F);
partsValue.Location = new System.Drawing.Point(80, 3); partsValue.Location = new System.Drawing.Point(90, 3);
partsValue.Margin = new System.Windows.Forms.Padding(0, 3, 0, 3); partsValue.Margin = new System.Windows.Forms.Padding(0, 3, 0, 3);
partsValue.Name = "partsValue"; partsValue.Name = "partsValue";
partsValue.Size = new System.Drawing.Size(13, 13); partsValue.Size = new System.Drawing.Size(13, 15);
partsValue.TabIndex = 1; partsValue.TabIndex = 1;
partsValue.Text = ""; partsValue.Text = "";
// //
@@ -122,10 +122,10 @@ namespace OpenNest.Forms
densityLabel.AutoSize = true; densityLabel.AutoSize = true;
densityLabel.Font = new System.Drawing.Font("Segoe UI", 9.75F, System.Drawing.FontStyle.Bold); densityLabel.Font = new System.Drawing.Font("Segoe UI", 9.75F, System.Drawing.FontStyle.Bold);
densityLabel.ForeColor = System.Drawing.Color.FromArgb(51, 51, 51); densityLabel.ForeColor = System.Drawing.Color.FromArgb(51, 51, 51);
densityLabel.Location = new System.Drawing.Point(0, 22); densityLabel.Location = new System.Drawing.Point(0, 26);
densityLabel.Margin = new System.Windows.Forms.Padding(0, 3, 5, 3); densityLabel.Margin = new System.Windows.Forms.Padding(0, 3, 5, 3);
densityLabel.Name = "densityLabel"; densityLabel.Name = "densityLabel";
densityLabel.Size = new System.Drawing.Size(49, 13); densityLabel.Size = new System.Drawing.Size(59, 17);
densityLabel.TabIndex = 2; densityLabel.TabIndex = 2;
densityLabel.Text = "Density:"; densityLabel.Text = "Density:";
// //
@@ -134,10 +134,10 @@ namespace OpenNest.Forms
densityPanel.AutoSize = true; densityPanel.AutoSize = true;
densityPanel.Controls.Add(densityValue); densityPanel.Controls.Add(densityValue);
densityPanel.Controls.Add(densityBar); densityPanel.Controls.Add(densityBar);
densityPanel.Location = new System.Drawing.Point(80, 19); densityPanel.Location = new System.Drawing.Point(90, 23);
densityPanel.Margin = new System.Windows.Forms.Padding(0); densityPanel.Margin = new System.Windows.Forms.Padding(0);
densityPanel.Name = "densityPanel"; densityPanel.Name = "densityPanel";
densityPanel.Size = new System.Drawing.Size(311, 19); densityPanel.Size = new System.Drawing.Size(262, 21);
densityPanel.TabIndex = 3; densityPanel.TabIndex = 3;
densityPanel.WrapContents = false; densityPanel.WrapContents = false;
// //
@@ -148,7 +148,7 @@ namespace OpenNest.Forms
densityValue.Location = new System.Drawing.Point(0, 3); densityValue.Location = new System.Drawing.Point(0, 3);
densityValue.Margin = new System.Windows.Forms.Padding(0, 3, 8, 3); densityValue.Margin = new System.Windows.Forms.Padding(0, 3, 8, 3);
densityValue.Name = "densityValue"; densityValue.Name = "densityValue";
densityValue.Size = new System.Drawing.Size(13, 13); densityValue.Size = new System.Drawing.Size(13, 15);
densityValue.TabIndex = 0; densityValue.TabIndex = 0;
densityValue.Text = ""; densityValue.Text = "";
// //
@@ -157,7 +157,7 @@ namespace OpenNest.Forms
densityBar.Location = new System.Drawing.Point(21, 5); densityBar.Location = new System.Drawing.Point(21, 5);
densityBar.Margin = new System.Windows.Forms.Padding(0, 5, 0, 0); densityBar.Margin = new System.Windows.Forms.Padding(0, 5, 0, 0);
densityBar.Name = "densityBar"; densityBar.Name = "densityBar";
densityBar.Size = new System.Drawing.Size(290, 8); densityBar.Size = new System.Drawing.Size(241, 8);
densityBar.TabIndex = 1; densityBar.TabIndex = 1;
densityBar.Value = 0D; densityBar.Value = 0D;
// //
@@ -166,10 +166,10 @@ namespace OpenNest.Forms
nestedAreaLabel.AutoSize = true; nestedAreaLabel.AutoSize = true;
nestedAreaLabel.Font = new System.Drawing.Font("Segoe UI", 9.75F, System.Drawing.FontStyle.Bold); nestedAreaLabel.Font = new System.Drawing.Font("Segoe UI", 9.75F, System.Drawing.FontStyle.Bold);
nestedAreaLabel.ForeColor = System.Drawing.Color.FromArgb(51, 51, 51); nestedAreaLabel.ForeColor = System.Drawing.Color.FromArgb(51, 51, 51);
nestedAreaLabel.Location = new System.Drawing.Point(0, 41); nestedAreaLabel.Location = new System.Drawing.Point(0, 49);
nestedAreaLabel.Margin = new System.Windows.Forms.Padding(0, 3, 5, 3); nestedAreaLabel.Margin = new System.Windows.Forms.Padding(0, 3, 5, 3);
nestedAreaLabel.Name = "nestedAreaLabel"; nestedAreaLabel.Name = "nestedAreaLabel";
nestedAreaLabel.Size = new System.Drawing.Size(47, 13); nestedAreaLabel.Size = new System.Drawing.Size(55, 17);
nestedAreaLabel.TabIndex = 4; nestedAreaLabel.TabIndex = 4;
nestedAreaLabel.Text = "Nested:"; nestedAreaLabel.Text = "Nested:";
// //
@@ -177,10 +177,10 @@ namespace OpenNest.Forms
// //
nestedAreaValue.AutoSize = true; nestedAreaValue.AutoSize = true;
nestedAreaValue.Font = new System.Drawing.Font("Consolas", 9.75F); nestedAreaValue.Font = new System.Drawing.Font("Consolas", 9.75F);
nestedAreaValue.Location = new System.Drawing.Point(80, 41); nestedAreaValue.Location = new System.Drawing.Point(90, 49);
nestedAreaValue.Margin = new System.Windows.Forms.Padding(0, 3, 0, 3); nestedAreaValue.Margin = new System.Windows.Forms.Padding(0, 3, 0, 3);
nestedAreaValue.Name = "nestedAreaValue"; nestedAreaValue.Name = "nestedAreaValue";
nestedAreaValue.Size = new System.Drawing.Size(13, 13); nestedAreaValue.Size = new System.Drawing.Size(13, 15);
nestedAreaValue.TabIndex = 5; nestedAreaValue.TabIndex = 5;
nestedAreaValue.Text = ""; nestedAreaValue.Text = "";
// //
@@ -193,7 +193,7 @@ namespace OpenNest.Forms
resultsHeader.Location = new System.Drawing.Point(14, 10); resultsHeader.Location = new System.Drawing.Point(14, 10);
resultsHeader.Name = "resultsHeader"; resultsHeader.Name = "resultsHeader";
resultsHeader.Padding = new System.Windows.Forms.Padding(0, 0, 0, 4); resultsHeader.Padding = new System.Windows.Forms.Padding(0, 0, 0, 4);
resultsHeader.Size = new System.Drawing.Size(56, 19); resultsHeader.Size = new System.Drawing.Size(65, 23);
resultsHeader.TabIndex = 0; resultsHeader.TabIndex = 0;
resultsHeader.Text = "RESULTS"; resultsHeader.Text = "RESULTS";
// //
@@ -203,7 +203,7 @@ namespace OpenNest.Forms
statusPanel.Controls.Add(statusTable); statusPanel.Controls.Add(statusTable);
statusPanel.Controls.Add(statusHeader); statusPanel.Controls.Add(statusHeader);
statusPanel.Dock = System.Windows.Forms.DockStyle.Top; statusPanel.Dock = System.Windows.Forms.DockStyle.Top;
statusPanel.Location = new System.Drawing.Point(0, 165); statusPanel.Location = new System.Drawing.Point(0, 180);
statusPanel.Name = "statusPanel"; statusPanel.Name = "statusPanel";
statusPanel.Padding = new System.Windows.Forms.Padding(14, 10, 14, 10); statusPanel.Padding = new System.Windows.Forms.Padding(14, 10, 14, 10);
statusPanel.Size = new System.Drawing.Size(450, 115); statusPanel.Size = new System.Drawing.Size(450, 115);
@@ -222,13 +222,13 @@ namespace OpenNest.Forms
statusTable.Controls.Add(descriptionLabel, 0, 2); statusTable.Controls.Add(descriptionLabel, 0, 2);
statusTable.Controls.Add(descriptionValue, 1, 2); statusTable.Controls.Add(descriptionValue, 1, 2);
statusTable.Dock = System.Windows.Forms.DockStyle.Top; statusTable.Dock = System.Windows.Forms.DockStyle.Top;
statusTable.Location = new System.Drawing.Point(14, 29); statusTable.Location = new System.Drawing.Point(14, 33);
statusTable.Name = "statusTable"; statusTable.Name = "statusTable";
statusTable.RowCount = 3; statusTable.RowCount = 3;
statusTable.RowStyles.Add(new System.Windows.Forms.RowStyle()); statusTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
statusTable.RowStyles.Add(new System.Windows.Forms.RowStyle()); statusTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
statusTable.RowStyles.Add(new System.Windows.Forms.RowStyle()); statusTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
statusTable.Size = new System.Drawing.Size(422, 57); statusTable.Size = new System.Drawing.Size(422, 69);
statusTable.TabIndex = 1; statusTable.TabIndex = 1;
// //
// plateLabel // plateLabel
@@ -239,7 +239,7 @@ namespace OpenNest.Forms
plateLabel.Location = new System.Drawing.Point(0, 3); plateLabel.Location = new System.Drawing.Point(0, 3);
plateLabel.Margin = new System.Windows.Forms.Padding(0, 3, 5, 3); plateLabel.Margin = new System.Windows.Forms.Padding(0, 3, 5, 3);
plateLabel.Name = "plateLabel"; plateLabel.Name = "plateLabel";
plateLabel.Size = new System.Drawing.Size(36, 13); plateLabel.Size = new System.Drawing.Size(43, 17);
plateLabel.TabIndex = 0; plateLabel.TabIndex = 0;
plateLabel.Text = "Plate:"; plateLabel.Text = "Plate:";
// //
@@ -247,10 +247,10 @@ namespace OpenNest.Forms
// //
plateValue.AutoSize = true; plateValue.AutoSize = true;
plateValue.Font = new System.Drawing.Font("Consolas", 9.75F); plateValue.Font = new System.Drawing.Font("Consolas", 9.75F);
plateValue.Location = new System.Drawing.Point(80, 3); plateValue.Location = new System.Drawing.Point(90, 3);
plateValue.Margin = new System.Windows.Forms.Padding(0, 3, 0, 3); plateValue.Margin = new System.Windows.Forms.Padding(0, 3, 0, 3);
plateValue.Name = "plateValue"; plateValue.Name = "plateValue";
plateValue.Size = new System.Drawing.Size(13, 13); plateValue.Size = new System.Drawing.Size(13, 15);
plateValue.TabIndex = 1; plateValue.TabIndex = 1;
plateValue.Text = ""; plateValue.Text = "";
// //
@@ -259,10 +259,10 @@ namespace OpenNest.Forms
elapsedLabel.AutoSize = true; elapsedLabel.AutoSize = true;
elapsedLabel.Font = new System.Drawing.Font("Segoe UI", 9.75F, System.Drawing.FontStyle.Bold); elapsedLabel.Font = new System.Drawing.Font("Segoe UI", 9.75F, System.Drawing.FontStyle.Bold);
elapsedLabel.ForeColor = System.Drawing.Color.FromArgb(51, 51, 51); elapsedLabel.ForeColor = System.Drawing.Color.FromArgb(51, 51, 51);
elapsedLabel.Location = new System.Drawing.Point(0, 22); elapsedLabel.Location = new System.Drawing.Point(0, 26);
elapsedLabel.Margin = new System.Windows.Forms.Padding(0, 3, 5, 3); elapsedLabel.Margin = new System.Windows.Forms.Padding(0, 3, 5, 3);
elapsedLabel.Name = "elapsedLabel"; elapsedLabel.Name = "elapsedLabel";
elapsedLabel.Size = new System.Drawing.Size(50, 13); elapsedLabel.Size = new System.Drawing.Size(59, 17);
elapsedLabel.TabIndex = 2; elapsedLabel.TabIndex = 2;
elapsedLabel.Text = "Elapsed:"; elapsedLabel.Text = "Elapsed:";
// //
@@ -270,10 +270,10 @@ namespace OpenNest.Forms
// //
elapsedValue.AutoSize = true; elapsedValue.AutoSize = true;
elapsedValue.Font = new System.Drawing.Font("Consolas", 9.75F); elapsedValue.Font = new System.Drawing.Font("Consolas", 9.75F);
elapsedValue.Location = new System.Drawing.Point(80, 22); elapsedValue.Location = new System.Drawing.Point(90, 26);
elapsedValue.Margin = new System.Windows.Forms.Padding(0, 3, 0, 3); elapsedValue.Margin = new System.Windows.Forms.Padding(0, 3, 0, 3);
elapsedValue.Name = "elapsedValue"; elapsedValue.Name = "elapsedValue";
elapsedValue.Size = new System.Drawing.Size(31, 13); elapsedValue.Size = new System.Drawing.Size(35, 15);
elapsedValue.TabIndex = 3; elapsedValue.TabIndex = 3;
elapsedValue.Text = "0:00"; elapsedValue.Text = "0:00";
// //
@@ -282,10 +282,10 @@ namespace OpenNest.Forms
descriptionLabel.AutoSize = true; descriptionLabel.AutoSize = true;
descriptionLabel.Font = new System.Drawing.Font("Segoe UI", 9.75F, System.Drawing.FontStyle.Bold); descriptionLabel.Font = new System.Drawing.Font("Segoe UI", 9.75F, System.Drawing.FontStyle.Bold);
descriptionLabel.ForeColor = System.Drawing.Color.FromArgb(51, 51, 51); descriptionLabel.ForeColor = System.Drawing.Color.FromArgb(51, 51, 51);
descriptionLabel.Location = new System.Drawing.Point(0, 41); descriptionLabel.Location = new System.Drawing.Point(0, 49);
descriptionLabel.Margin = new System.Windows.Forms.Padding(0, 3, 5, 3); descriptionLabel.Margin = new System.Windows.Forms.Padding(0, 3, 5, 3);
descriptionLabel.Name = "descriptionLabel"; descriptionLabel.Name = "descriptionLabel";
descriptionLabel.Size = new System.Drawing.Size(40, 13); descriptionLabel.Size = new System.Drawing.Size(49, 17);
descriptionLabel.TabIndex = 4; descriptionLabel.TabIndex = 4;
descriptionLabel.Text = "Detail:"; descriptionLabel.Text = "Detail:";
// //
@@ -293,10 +293,10 @@ namespace OpenNest.Forms
// //
descriptionValue.AutoSize = true; descriptionValue.AutoSize = true;
descriptionValue.Font = new System.Drawing.Font("Segoe UI", 9.75F); descriptionValue.Font = new System.Drawing.Font("Segoe UI", 9.75F);
descriptionValue.Location = new System.Drawing.Point(80, 41); descriptionValue.Location = new System.Drawing.Point(90, 49);
descriptionValue.Margin = new System.Windows.Forms.Padding(0, 3, 0, 3); descriptionValue.Margin = new System.Windows.Forms.Padding(0, 3, 0, 3);
descriptionValue.Name = "descriptionValue"; descriptionValue.Name = "descriptionValue";
descriptionValue.Size = new System.Drawing.Size(18, 13); descriptionValue.Size = new System.Drawing.Size(20, 17);
descriptionValue.TabIndex = 5; descriptionValue.TabIndex = 5;
descriptionValue.Text = ""; descriptionValue.Text = "";
// //
@@ -309,7 +309,7 @@ namespace OpenNest.Forms
statusHeader.Location = new System.Drawing.Point(14, 10); statusHeader.Location = new System.Drawing.Point(14, 10);
statusHeader.Name = "statusHeader"; statusHeader.Name = "statusHeader";
statusHeader.Padding = new System.Windows.Forms.Padding(0, 0, 0, 4); statusHeader.Padding = new System.Windows.Forms.Padding(0, 0, 0, 4);
statusHeader.Size = new System.Drawing.Size(50, 19); statusHeader.Size = new System.Drawing.Size(59, 23);
statusHeader.TabIndex = 0; statusHeader.TabIndex = 0;
statusHeader.Text = "STATUS"; statusHeader.Text = "STATUS";
// //
@@ -320,7 +320,7 @@ namespace OpenNest.Forms
buttonPanel.Controls.Add(acceptButton); buttonPanel.Controls.Add(acceptButton);
buttonPanel.Dock = System.Windows.Forms.DockStyle.Top; buttonPanel.Dock = System.Windows.Forms.DockStyle.Top;
buttonPanel.FlowDirection = System.Windows.Forms.FlowDirection.RightToLeft; buttonPanel.FlowDirection = System.Windows.Forms.FlowDirection.RightToLeft;
buttonPanel.Location = new System.Drawing.Point(0, 265); buttonPanel.Location = new System.Drawing.Point(0, 295);
buttonPanel.Name = "buttonPanel"; buttonPanel.Name = "buttonPanel";
buttonPanel.Padding = new System.Windows.Forms.Padding(9, 6, 9, 6); buttonPanel.Padding = new System.Windows.Forms.Padding(9, 6, 9, 6);
buttonPanel.Size = new System.Drawing.Size(450, 45); buttonPanel.Size = new System.Drawing.Size(450, 45);
+1 -14
View File
@@ -73,7 +73,7 @@ namespace OpenNest.Forms
descriptionValue.Text = !string.IsNullOrEmpty(progress.Description) descriptionValue.Text = !string.IsNullOrEmpty(progress.Description)
? progress.Description ? progress.Description
: FormatPhase(progress.Phase); : progress.Phase.DisplayName();
} }
public void ShowCompleted() public void ShowCompleted()
@@ -196,18 +196,5 @@ namespace OpenNest.Forms
return DensityMidColor; return DensityMidColor;
return DensityHighColor; return DensityHighColor;
} }
private static string FormatPhase(NestPhase phase)
{
switch (phase)
{
case NestPhase.Linear: return "Trying rotations...";
case NestPhase.RectBestFit: return "Trying best fit...";
case NestPhase.Pairs: return "Trying pairs...";
case NestPhase.Extents: return "Trying extents...";
case NestPhase.Nfp: return "Trying NFP...";
default: return phase.ToString();
}
}
} }
} }