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)
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<Part>();
}
@@ -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,
});
}
}
}
@@ -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);
+12 -56
View File
@@ -7,74 +7,30 @@ namespace OpenNest
public static bool FindFrom2(double length1, double length2, double overallLength, out int count1, out int count2)
{
overallLength += Tolerance.Epsilon;
if (length1 > overallLength)
{
if (length2 > overallLength)
{
count1 = 0;
count2 = 0;
return false;
}
count1 = 0;
count2 = (int)System.Math.Floor(overallLength / length2);
return true;
}
var maxCount1 = (int)System.Math.Floor(overallLength / length1);
var bestRemnant = overallLength + 1;
if (length2 > overallLength)
for (var c1 = 0; c1 <= maxCount1; c1++)
{
count1 = (int)System.Math.Floor(overallLength / length1);
count2 = 0;
return true;
}
var remaining = overallLength - c1 * length1;
var c2 = (int)System.Math.Floor(remaining / length2);
var remnant = remaining - c2 * length2;
var maxCountLength1 = (int)System.Math.Floor(overallLength / length1);
if (!(remnant < bestRemnant))
continue;
count1 = maxCountLength1;
count2 = 0;
var remnant = overallLength - maxCountLength1 * length1;
count1 = c1;
count2 = c2;
bestRemnant = remnant;
if (remnant.IsEqualTo(0))
return true;
for (int countLength1 = 0; countLength1 <= maxCountLength1; ++countLength1)
{
var remnant1 = overallLength - countLength1 * length1;
if (remnant1 >= length2)
{
var countLength2 = (int)System.Math.Floor(remnant1 / length2);
var remnant2 = remnant1 - length2 * countLength2;
if (!(remnant2 < remnant))
continue;
count1 = countLength1;
count2 = countLength2;
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)
return new List<Part>();
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;
}
+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,
CancellationToken token = default,
IProgress<NestProgress> progress = null,
int plateNumber = 0)
int plateNumber = 0,
Func<NestItem, Box, List<Part>> widthFillFunc = null)
{
if (items == null || items.Count == 0)
return new IterativeShrinkResult();
@@ -72,6 +73,8 @@ namespace OpenNest.Engine.Fill
// include them in progress reports.
var placedSoFar = new List<Part>();
var wFillFunc = widthFillFunc ?? fillFunc;
Func<NestItem, Box, List<Part>> shrinkWrapper = (ni, box) =>
{
var target = ni.Quantity > 0 ? ni.Quantity : 0;
@@ -84,7 +87,7 @@ namespace OpenNest.Engine.Fill
Parallel.Invoke(
() => heightResult = ShrinkFiller.Shrink(fillFunc, ni, box, spacing, ShrinkAxis.Height, token,
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)
);
@@ -108,8 +111,15 @@ namespace OpenNest.Engine.Fill
var allParts = new List<Part>(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.
+83 -27
View File
@@ -7,6 +7,7 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using OpenNest.Engine;
namespace OpenNest.Engine.Fill
@@ -32,13 +33,15 @@ namespace OpenNest.Engine.Fill
private readonly Size plateSize;
private readonly double partSpacing;
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.plateSize = plate.Size;
this.partSpacing = plate.PartSpacing;
this.comparer = comparer ?? new DefaultFillComparer();
this.dedup = dedup ?? new GridDedup();
}
public PairFillResult Fill(NestItem item, Box workArea,
@@ -68,32 +71,60 @@ namespace OpenNest.Engine.Fill
List<Part> best = null;
var sinceImproved = 0;
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
{
for (var i = 0; i < candidates.Count; i++)
for (var batchStart = 0; batchStart < candidates.Count; batchStart += batchSize)
{
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 =>
{
best = filled;
results[j] = EvaluateCandidate(
candidates[batchStart + j], drawing, batchWorkArea,
minCountToBeat, maxUtilization, partArea, token);
});
for (var j = 0; j < batchCount; j++)
{
if (comparer.IsBetter(results[j], best, effectiveWorkArea))
{
best = results[j];
sinceImproved = 0;
effectiveWorkArea = TryReduceWorkArea(filled, targetCount, workArea, effectiveWorkArea);
effectiveWorkArea = TryReduceWorkArea(best, targetCount, workArea, effectiveWorkArea);
}
else
{
sinceImproved++;
}
NestEngineBase.ReportProgress(progress, NestPhase.Pairs, plateNumber, best, workArea,
$"Pairs: {i + 1}/{candidates.Count} candidates, best = {best?.Count ?? 0} parts");
if (i + 1 >= EarlyExitMinTried && sinceImproved >= EarlyExitStaleLimit)
NestEngineBase.ReportProgress(progress, new ProgressReport
{
Debug.WriteLine($"[PairFiller] Early exit at {i + 1}/{candidates.Count} — no improvement in last {sinceImproved} candidates");
Phase = NestPhase.Pairs,
PlateNumber = plateNumber,
Parts = best,
WorkArea = workArea,
Description = $"Pairs: {batchStart + j + 1}/{candidates.Count} candidates, best = {best?.Count ?? 0} parts",
});
}
if (batchEnd >= EarlyExitMinTried && sinceImproved >= EarlyExitStaleLimit)
{
Debug.WriteLine($"[PairFiller] Early exit at {batchEnd}/{candidates.Count} — no improvement in last {sinceImproved} candidates");
break;
}
}
@@ -102,6 +133,10 @@ namespace OpenNest.Engine.Fill
{
Debug.WriteLine("[PairFiller] Cancelled mid-phase, using results so far");
}
finally
{
FillStrategyRegistry.SetEnabled(null);
}
Debug.WriteLine($"[PairFiller] Best pair result: {best?.Count ?? 0} parts");
return best ?? new List<Part>();
@@ -145,7 +180,8 @@ namespace OpenNest.Engine.Fill
}
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 angles = BuildTilingAngles(candidate);
@@ -162,6 +198,9 @@ namespace OpenNest.Engine.Fill
var engine = new FillLinear(workArea, partSpacing);
foreach (var dir in new[] { NestDirection.Horizontal, NestDirection.Vertical })
{
if (!dedup.TryAdd(pattern.BoundingBox, workArea, dir))
continue;
var gridParts = engine.Fill(pattern, dir);
if (gridParts != null && gridParts.Count > 0)
grids.Add((gridParts, dir));
@@ -174,17 +213,34 @@ namespace OpenNest.Engine.Fill
// Sort by count descending so we try the best grids first
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
List<Part> best = null;
var maxRemnantEstimate = EstimateMaxRemnantParts(drawing, workArea);
foreach (var (gridParts, dir) in grids)
{
token.ThrowIfCancellationRequested();
// If this grid + max possible remnant can't beat current best, skip
if (best != null && gridParts.Count + maxRemnantEstimate <= best.Count)
if (best != null)
{
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);
List<Part> total;
@@ -206,12 +262,20 @@ namespace OpenNest.Engine.Fill
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 partArea = System.Math.Max(partBox.Width * partBox.Length, 1);
var remnantArea = workArea.Area() * 0.3; // remnant is at most ~30% of work area
return (int)(remnantArea / partArea) + 1;
var gridBox = ((IEnumerable<IBoundable>)gridParts).GetBoundingBox();
// L-shaped remnant: top strip (full width) + right strip (grid height only)
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,
@@ -257,9 +321,6 @@ namespace OpenNest.Engine.Fill
return cachedResult;
}
FillStrategyRegistry.SetEnabled("Pairs", "RectBestFit", "Extents", "Linear");
try
{
var remnantEngine = NestEngineRegistry.Create(plate);
var item = new NestItem { Drawing = drawing };
var parts = remnantEngine.Fill(item, remnantBox, null, token);
@@ -275,11 +336,6 @@ namespace OpenNest.Engine.Fill
return null;
}
finally
{
FillStrategyRegistry.SetEnabled(null);
}
}
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}";
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,
});
}
/// <summary>
+14 -3
View File
@@ -20,6 +20,7 @@ public class StripeFiller
private readonly FillContext _context;
private readonly NestDirection _primaryAxis;
private readonly IFillComparer _comparer;
private readonly GridDedup _dedup;
/// <summary>
/// When true, only complete stripes are placed — no partial rows/columns.
@@ -38,6 +39,7 @@ public class StripeFiller
_context = context;
_primaryAxis = primaryAxis;
_comparer = context.Policy?.Comparer ?? new DefaultFillComparer();
_dedup = GridDedup.GetOrCreate(context.SharedState);
}
public List<Part> Fill()
@@ -93,9 +95,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<Part>();
@@ -110,6 +117,10 @@ public class StripeFiller
var rotatedPattern = FillHelpers.BuildRotatedPattern(pairParts, angle);
var perpDim = GetDimension(rotatedPattern.BoundingBox, perpAxis);
var stripeBox = MakeStripeBox(workArea, perpDim, primaryAxis);
if (!_dedup.TryAdd(rotatedPattern.BoundingBox, workArea, primaryAxis))
return null;
var stripeEngine = new FillLinear(stripeBox, spacing);
var stripeParts = stripeEngine.Fill(rotatedPattern, primaryAxis);
+12 -53
View File
@@ -210,55 +210,26 @@ namespace OpenNest
// --- Protected utilities ---
internal static void ReportProgress(
IProgress<NestProgress> progress,
NestPhase phase,
int plateNumber,
List<Part> best,
Box workArea,
string description,
bool isOverallBest = false)
IProgress<NestProgress> 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<Part>(best.Count);
var totalPartArea = 0.0;
foreach (var part in best)
{
var clonedParts = new List<Part>(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<string>(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();
}
}
}
}
+123 -12
View File
@@ -1,16 +1,52 @@
using OpenNest.Geometry;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.Reflection;
namespace OpenNest
{
[AttributeUsage(AttributeTargets.Field)]
internal class ShortNameAttribute(string name) : Attribute
{
public string Name { get; } = name;
}
public enum NestPhase
{
Linear,
RectBestFit,
Pairs,
Nfp,
Extents,
Custom
[Description("Trying rotations..."), ShortName("Linear")] Linear,
[Description("Trying best fit..."), ShortName("BestFit")] RectBestFit,
[Description("Trying pairs..."), ShortName("Pairs")] Pairs,
[Description("Trying NFP..."), ShortName("NFP")] Nfp,
[Description("Trying extents..."), ShortName("Extents")] Extents,
[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
@@ -34,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<Part> 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<Part> BestParts { get; set; }
private List<Part> bestParts;
public List<Part> 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<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");
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;
}
+9 -2
View File
@@ -277,8 +277,15 @@ namespace OpenNest.Engine.Nfp
private static void ReportBest(IProgress<NestProgress> progress, List<Part> 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,
});
}
}
}
@@ -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<Part>();
@@ -12,7 +12,8 @@ namespace OpenNest.Engine.Strategies
public List<Part> Fill(FillContext context)
{
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,
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.
if (fillItems.Count > 0)
{
// Pass progress through so the UI shows intermediate results
// during the initial BestFitCache computation and fill phases.
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
// Use direction-specific engines: height shrink benefits from
// minimizing Y-extent, width shrink from minimizing X-extent.
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);
};
var shrinkResult = IterativeShrinkFiller.Fill(
fillItems, workArea, fillFunc, Plate.PartSpacing, token,
progress, PlateNumber);
fillItems, workArea, heightFillFunc, Plate.PartSpacing, token,
progress, PlateNumber, widthFillFunc);
allParts.AddRange(shrinkResult.Parts);
+2 -2
View File
@@ -18,7 +18,7 @@ public class AccumulatingProgressTests
var accumulating = new AccumulatingProgress(inner, previous);
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.Equal(2, inner.Last.BestParts.Count);
@@ -32,7 +32,7 @@ public class AccumulatingProgressTests
var accumulating = new AccumulatingProgress(inner, new List<Part>());
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.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)
{
base.OnPaint(e);
@@ -134,7 +124,7 @@ namespace OpenNest.Controls
}
// Label
var label = GetDisplayName(phase);
var label = phase.ShortName();
var font = isVisited || isActive ? BoldLabelFont : LabelFont;
var brush = isVisited || isActive ? activeTextBrush : pendingTextBrush;
var labelSize = g.MeasureString(label, font);
+3 -10
View File
@@ -1098,23 +1098,16 @@ namespace OpenNest.Controls
var bounds = parts.GetBoundingBox();
var center = bounds.Center;
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 basePart = part.BasePart;
if (rotatedPrograms.Add(basePart.Program))
basePart.Program.Rotate(angle);
part.Location = part.Location.Rotate(angle, center);
basePart.UpdateBounds();
part.BasePart.Rotate(angle, center);
}
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);
}
+32 -32
View File
@@ -85,13 +85,13 @@ namespace OpenNest.Forms
resultsTable.Controls.Add(nestedAreaLabel, 0, 2);
resultsTable.Controls.Add(nestedAreaValue, 1, 2);
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.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.Size = new System.Drawing.Size(422, 57);
resultsTable.Size = new System.Drawing.Size(422, 69);
resultsTable.TabIndex = 1;
//
// partsLabel
@@ -102,7 +102,7 @@ namespace OpenNest.Forms
partsLabel.Location = new System.Drawing.Point(0, 3);
partsLabel.Margin = new System.Windows.Forms.Padding(0, 3, 5, 3);
partsLabel.Name = "partsLabel";
partsLabel.Size = new System.Drawing.Size(36, 13);
partsLabel.Size = new System.Drawing.Size(43, 17);
partsLabel.TabIndex = 0;
partsLabel.Text = "Parts:";
//
@@ -110,10 +110,10 @@ namespace OpenNest.Forms
//
partsValue.AutoSize = true;
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.Name = "partsValue";
partsValue.Size = new System.Drawing.Size(13, 13);
partsValue.Size = new System.Drawing.Size(13, 15);
partsValue.TabIndex = 1;
partsValue.Text = "";
//
@@ -122,10 +122,10 @@ namespace OpenNest.Forms
densityLabel.AutoSize = true;
densityLabel.Font = new System.Drawing.Font("Segoe UI", 9.75F, System.Drawing.FontStyle.Bold);
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.Name = "densityLabel";
densityLabel.Size = new System.Drawing.Size(49, 13);
densityLabel.Size = new System.Drawing.Size(59, 17);
densityLabel.TabIndex = 2;
densityLabel.Text = "Density:";
//
@@ -134,10 +134,10 @@ namespace OpenNest.Forms
densityPanel.AutoSize = true;
densityPanel.Controls.Add(densityValue);
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.Name = "densityPanel";
densityPanel.Size = new System.Drawing.Size(311, 19);
densityPanel.Size = new System.Drawing.Size(262, 21);
densityPanel.TabIndex = 3;
densityPanel.WrapContents = false;
//
@@ -148,7 +148,7 @@ namespace OpenNest.Forms
densityValue.Location = new System.Drawing.Point(0, 3);
densityValue.Margin = new System.Windows.Forms.Padding(0, 3, 8, 3);
densityValue.Name = "densityValue";
densityValue.Size = new System.Drawing.Size(13, 13);
densityValue.Size = new System.Drawing.Size(13, 15);
densityValue.TabIndex = 0;
densityValue.Text = "";
//
@@ -157,7 +157,7 @@ namespace OpenNest.Forms
densityBar.Location = new System.Drawing.Point(21, 5);
densityBar.Margin = new System.Windows.Forms.Padding(0, 5, 0, 0);
densityBar.Name = "densityBar";
densityBar.Size = new System.Drawing.Size(290, 8);
densityBar.Size = new System.Drawing.Size(241, 8);
densityBar.TabIndex = 1;
densityBar.Value = 0D;
//
@@ -166,10 +166,10 @@ namespace OpenNest.Forms
nestedAreaLabel.AutoSize = true;
nestedAreaLabel.Font = new System.Drawing.Font("Segoe UI", 9.75F, System.Drawing.FontStyle.Bold);
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.Name = "nestedAreaLabel";
nestedAreaLabel.Size = new System.Drawing.Size(47, 13);
nestedAreaLabel.Size = new System.Drawing.Size(55, 17);
nestedAreaLabel.TabIndex = 4;
nestedAreaLabel.Text = "Nested:";
//
@@ -177,10 +177,10 @@ namespace OpenNest.Forms
//
nestedAreaValue.AutoSize = true;
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.Name = "nestedAreaValue";
nestedAreaValue.Size = new System.Drawing.Size(13, 13);
nestedAreaValue.Size = new System.Drawing.Size(13, 15);
nestedAreaValue.TabIndex = 5;
nestedAreaValue.Text = "";
//
@@ -193,7 +193,7 @@ namespace OpenNest.Forms
resultsHeader.Location = new System.Drawing.Point(14, 10);
resultsHeader.Name = "resultsHeader";
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.Text = "RESULTS";
//
@@ -203,7 +203,7 @@ namespace OpenNest.Forms
statusPanel.Controls.Add(statusTable);
statusPanel.Controls.Add(statusHeader);
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.Padding = new System.Windows.Forms.Padding(14, 10, 14, 10);
statusPanel.Size = new System.Drawing.Size(450, 115);
@@ -222,13 +222,13 @@ namespace OpenNest.Forms
statusTable.Controls.Add(descriptionLabel, 0, 2);
statusTable.Controls.Add(descriptionValue, 1, 2);
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.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.Size = new System.Drawing.Size(422, 57);
statusTable.Size = new System.Drawing.Size(422, 69);
statusTable.TabIndex = 1;
//
// plateLabel
@@ -239,7 +239,7 @@ namespace OpenNest.Forms
plateLabel.Location = new System.Drawing.Point(0, 3);
plateLabel.Margin = new System.Windows.Forms.Padding(0, 3, 5, 3);
plateLabel.Name = "plateLabel";
plateLabel.Size = new System.Drawing.Size(36, 13);
plateLabel.Size = new System.Drawing.Size(43, 17);
plateLabel.TabIndex = 0;
plateLabel.Text = "Plate:";
//
@@ -247,10 +247,10 @@ namespace OpenNest.Forms
//
plateValue.AutoSize = true;
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.Name = "plateValue";
plateValue.Size = new System.Drawing.Size(13, 13);
plateValue.Size = new System.Drawing.Size(13, 15);
plateValue.TabIndex = 1;
plateValue.Text = "";
//
@@ -259,10 +259,10 @@ namespace OpenNest.Forms
elapsedLabel.AutoSize = true;
elapsedLabel.Font = new System.Drawing.Font("Segoe UI", 9.75F, System.Drawing.FontStyle.Bold);
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.Name = "elapsedLabel";
elapsedLabel.Size = new System.Drawing.Size(50, 13);
elapsedLabel.Size = new System.Drawing.Size(59, 17);
elapsedLabel.TabIndex = 2;
elapsedLabel.Text = "Elapsed:";
//
@@ -270,10 +270,10 @@ namespace OpenNest.Forms
//
elapsedValue.AutoSize = true;
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.Name = "elapsedValue";
elapsedValue.Size = new System.Drawing.Size(31, 13);
elapsedValue.Size = new System.Drawing.Size(35, 15);
elapsedValue.TabIndex = 3;
elapsedValue.Text = "0:00";
//
@@ -282,10 +282,10 @@ namespace OpenNest.Forms
descriptionLabel.AutoSize = true;
descriptionLabel.Font = new System.Drawing.Font("Segoe UI", 9.75F, System.Drawing.FontStyle.Bold);
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.Name = "descriptionLabel";
descriptionLabel.Size = new System.Drawing.Size(40, 13);
descriptionLabel.Size = new System.Drawing.Size(49, 17);
descriptionLabel.TabIndex = 4;
descriptionLabel.Text = "Detail:";
//
@@ -293,10 +293,10 @@ namespace OpenNest.Forms
//
descriptionValue.AutoSize = true;
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.Name = "descriptionValue";
descriptionValue.Size = new System.Drawing.Size(18, 13);
descriptionValue.Size = new System.Drawing.Size(20, 17);
descriptionValue.TabIndex = 5;
descriptionValue.Text = "";
//
@@ -309,7 +309,7 @@ namespace OpenNest.Forms
statusHeader.Location = new System.Drawing.Point(14, 10);
statusHeader.Name = "statusHeader";
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.Text = "STATUS";
//
@@ -320,7 +320,7 @@ namespace OpenNest.Forms
buttonPanel.Controls.Add(acceptButton);
buttonPanel.Dock = System.Windows.Forms.DockStyle.Top;
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.Padding = new System.Windows.Forms.Padding(9, 6, 9, 6);
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)
? progress.Description
: FormatPhase(progress.Phase);
: progress.Phase.DisplayName();
}
public void ShowCompleted()
@@ -196,18 +196,5 @@ namespace OpenNest.Forms
return DensityMidColor;
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();
}
}
}
}