fix: improve fill progress reporting and engine pipeline
- Strategies now promote results to IsOverallBest when they beat the pipeline best, so the UI updates immediately on improvement rather than waiting for each phase to complete - PlateView only updates the main view on overall-best results, fixing intermediate angle-sweep layouts leaking to the plate display - Skip Row/Column strategies for rectangle parts (redundant with Linear) - Intercept Escape key at MainForm level via ProcessCmdKey so it always reaches the active PlateView regardless of focus state - Restore keyboard focus to PlateView after fill progress form closes - Remnant engines use SelectBestFitPair for orientation-aware pair selection; DefaultNestEngine tries both landscape and portrait pairs - RemnantFiller preserves more parts during topmost-part removal Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -139,28 +139,63 @@ namespace OpenNest
|
||||
var bestFits = BestFitCache.GetOrCompute(
|
||||
drawing, Plate.Size.Length, Plate.Size.Width, Plate.PartSpacing);
|
||||
|
||||
var best = bestFits.FirstOrDefault(r => r.Keep);
|
||||
var best = SelectBestFitPair(bestFits);
|
||||
if (best == null)
|
||||
return null;
|
||||
|
||||
var parts = best.BuildParts(drawing);
|
||||
// BuildParts produces landscape orientation (Width >= Height).
|
||||
// Try both landscape and portrait (90° rotated) and let the
|
||||
// engine's comparer pick the better orientation.
|
||||
var landscape = best.BuildParts(drawing);
|
||||
var portrait = RotatePair90(landscape);
|
||||
|
||||
// BuildParts positions at origin — offset to work area.
|
||||
var lFits = TryOffsetToWorkArea(landscape, workArea);
|
||||
var pFits = TryOffsetToWorkArea(portrait, workArea);
|
||||
|
||||
if (!lFits && !pFits)
|
||||
return null;
|
||||
if (lFits && pFits)
|
||||
return IsBetterFill(portrait, landscape, workArea) ? portrait : landscape;
|
||||
return lFits ? landscape : portrait;
|
||||
}
|
||||
|
||||
private static List<Part> RotatePair90(List<Part> parts)
|
||||
{
|
||||
var rotated = new List<Part>(parts.Count);
|
||||
foreach (var p in parts)
|
||||
rotated.Add((Part)p.Clone());
|
||||
|
||||
var bbox = ((IEnumerable<IBoundable>)rotated).GetBoundingBox();
|
||||
var center = bbox.Center;
|
||||
|
||||
foreach (var p in rotated)
|
||||
p.Rotate(-Angle.HalfPI, center);
|
||||
|
||||
var newBbox = ((IEnumerable<IBoundable>)rotated).GetBoundingBox();
|
||||
var offset = new Vector(-newBbox.Left, -newBbox.Bottom);
|
||||
foreach (var p in rotated)
|
||||
{
|
||||
p.Offset(offset);
|
||||
p.UpdateBounds();
|
||||
}
|
||||
|
||||
return rotated;
|
||||
}
|
||||
|
||||
private static bool TryOffsetToWorkArea(List<Part> parts, Box workArea)
|
||||
{
|
||||
var bbox = ((IEnumerable<IBoundable>)parts).GetBoundingBox();
|
||||
if (bbox.Width > workArea.Width + Tolerance.Epsilon ||
|
||||
bbox.Length > workArea.Length + Tolerance.Epsilon)
|
||||
return false;
|
||||
|
||||
var offset = workArea.Location - bbox.Location;
|
||||
foreach (var p in parts)
|
||||
{
|
||||
p.Offset(offset);
|
||||
p.UpdateBounds();
|
||||
}
|
||||
|
||||
// Verify pair fits in work area.
|
||||
bbox = ((IEnumerable<IBoundable>)parts).GetBoundingBox();
|
||||
if (bbox.Width > workArea.Width + Tolerance.Epsilon ||
|
||||
bbox.Length > workArea.Length + Tolerance.Epsilon)
|
||||
return null;
|
||||
|
||||
return parts;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -309,14 +344,18 @@ namespace OpenNest
|
||||
// during progress reporting.
|
||||
PhaseResults.Add(phaseResult);
|
||||
|
||||
if (context.Policy.Comparer.IsBetter(result, context.CurrentBest, context.WorkArea))
|
||||
// FillContext.ReportProgress updates CurrentBest during the
|
||||
// strategy's angle sweep. This catches strategies that return a
|
||||
// result without reporting it (e.g. RectBestFit).
|
||||
var improved = context.Policy.Comparer.IsBetter(result, context.CurrentBest, context.WorkArea);
|
||||
if (improved)
|
||||
{
|
||||
context.CurrentBest = result;
|
||||
context.CurrentBestScore = FillScore.Compute(result, context.WorkArea);
|
||||
context.WinnerPhase = strategy.Phase;
|
||||
}
|
||||
|
||||
if (context.CurrentBest != null && context.CurrentBest.Count > 0)
|
||||
if (improved && context.CurrentBest != null && context.CurrentBest.Count > 0)
|
||||
{
|
||||
ReportProgress(context.Progress, new ProgressReport
|
||||
{
|
||||
|
||||
@@ -106,7 +106,7 @@ namespace OpenNest.Engine.Fill
|
||||
// rectangular obstacle boundary. Without this, gaps between
|
||||
// individual bounding boxes cause the next drawing to fill
|
||||
// into inter-row spaces, producing an interleaved layout.
|
||||
if (placed.Count > 1)
|
||||
if (placed.Count > 2)
|
||||
RemoveTopmostPart(placed);
|
||||
|
||||
allParts.AddRange(placed);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Engine;
|
||||
using OpenNest.Engine.BestFit;
|
||||
using OpenNest.Engine.Fill;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
@@ -26,6 +27,20 @@ namespace OpenNest
|
||||
|
||||
public override ShrinkAxis TrimAxis => ShrinkAxis.Length;
|
||||
|
||||
protected override BestFitResult SelectBestFitPair(List<BestFitResult> results)
|
||||
{
|
||||
BestFitResult best = null;
|
||||
|
||||
foreach (var r in results)
|
||||
{
|
||||
if (!r.Keep) continue;
|
||||
if (best == null || r.BoundingHeight < best.BoundingHeight)
|
||||
best = r;
|
||||
}
|
||||
|
||||
return best;
|
||||
}
|
||||
|
||||
public override List<double> BuildAngles(NestItem item, ClassificationResult classification, Box workArea)
|
||||
{
|
||||
var baseAngles = new List<double> { classification.PrimaryAngle, classification.PrimaryAngle + Angle.HalfPI };
|
||||
|
||||
@@ -56,6 +56,11 @@ namespace OpenNest
|
||||
|
||||
protected FillPolicy BuildPolicy() => new FillPolicy(Comparer, PreferredDirection);
|
||||
|
||||
protected virtual BestFitResult SelectBestFitPair(List<BestFitResult> results)
|
||||
{
|
||||
return results.FirstOrDefault(r => r.Keep);
|
||||
}
|
||||
|
||||
// --- Virtual methods (side-effect-free, return parts) ---
|
||||
|
||||
public virtual List<Part> Fill(NestItem item, Box workArea,
|
||||
@@ -333,7 +338,7 @@ namespace OpenNest
|
||||
|
||||
var bestFits = BestFitCache.GetOrCompute(
|
||||
item.Drawing, Plate.Size.Length, Plate.Size.Width, Plate.PartSpacing);
|
||||
var bestFit = bestFits.FirstOrDefault(r => r.Keep);
|
||||
var bestFit = SelectBestFitPair(bestFits);
|
||||
if (bestFit == null) continue;
|
||||
|
||||
var parts = bestFit.BuildParts(item.Drawing);
|
||||
|
||||
@@ -11,6 +11,9 @@ public class ColumnFillStrategy : IFillStrategy
|
||||
|
||||
public List<Part> Fill(FillContext context)
|
||||
{
|
||||
if (context.PartType == PartType.Rectangle)
|
||||
return null;
|
||||
|
||||
var filler = new StripeFiller(context, NestDirection.Vertical) { CompleteStripesOnly = true };
|
||||
return filler.Fill();
|
||||
}
|
||||
|
||||
@@ -32,16 +32,29 @@ namespace OpenNest.Engine.Strategies
|
||||
/// <summary>
|
||||
/// Standard progress reporting for strategies and fillers. Reports intermediate
|
||||
/// results using the current ActivePhase, PlateNumber, and WorkArea.
|
||||
/// When the reported parts beat the current pipeline best, promotes the
|
||||
/// result to IsOverallBest so the UI updates immediately.
|
||||
/// </summary>
|
||||
public void ReportProgress(List<Part> parts, string description)
|
||||
{
|
||||
var isNewBest = parts != null && parts.Count > 0
|
||||
&& Policy.Comparer.IsBetter(parts, CurrentBest, WorkArea);
|
||||
|
||||
if (isNewBest)
|
||||
{
|
||||
CurrentBest = parts;
|
||||
CurrentBestScore = FillScore.Compute(parts, WorkArea);
|
||||
WinnerPhase = ActivePhase;
|
||||
}
|
||||
|
||||
NestEngineBase.ReportProgress(Progress, new ProgressReport
|
||||
{
|
||||
Phase = ActivePhase,
|
||||
PlateNumber = PlateNumber,
|
||||
Parts = parts,
|
||||
Parts = isNewBest ? parts : CurrentBest,
|
||||
WorkArea = WorkArea,
|
||||
Description = description,
|
||||
IsOverallBest = isNewBest,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,9 @@ public class RowFillStrategy : IFillStrategy
|
||||
|
||||
public List<Part> Fill(FillContext context)
|
||||
{
|
||||
if (context.PartType == PartType.Rectangle)
|
||||
return null;
|
||||
|
||||
var filler = new StripeFiller(context, NestDirection.Horizontal) { CompleteStripesOnly = true };
|
||||
return filler.Fill();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Engine;
|
||||
using OpenNest.Engine.BestFit;
|
||||
using OpenNest.Engine.Fill;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
@@ -24,6 +25,20 @@ namespace OpenNest
|
||||
|
||||
public override NestDirection? PreferredDirection => NestDirection.Horizontal;
|
||||
|
||||
protected override BestFitResult SelectBestFitPair(List<BestFitResult> results)
|
||||
{
|
||||
BestFitResult best = null;
|
||||
|
||||
foreach (var r in results)
|
||||
{
|
||||
if (!r.Keep) continue;
|
||||
if (best == null || r.BoundingHeight < best.BoundingHeight)
|
||||
best = r;
|
||||
}
|
||||
|
||||
return best;
|
||||
}
|
||||
|
||||
public override List<double> BuildAngles(NestItem item, ClassificationResult classification, Box workArea)
|
||||
{
|
||||
var baseAngles = new List<double> { classification.PrimaryAngle, classification.PrimaryAngle + Angle.HalfPI };
|
||||
|
||||
@@ -412,6 +412,16 @@ namespace OpenNest.Controls
|
||||
}
|
||||
}
|
||||
|
||||
public void ProcessEscapeKey()
|
||||
{
|
||||
if (currentAction.IsBusy())
|
||||
currentAction.CancelAction();
|
||||
else if (currentAction is ActionSelect && previousAction != null)
|
||||
RestorePreviousAction();
|
||||
else
|
||||
SetAction(typeof(ActionSelect));
|
||||
}
|
||||
|
||||
protected override bool ProcessDialogKey(Keys keyData)
|
||||
{
|
||||
// Only handle TAB, RETURN, ESC, and ARROW KEYS here.
|
||||
@@ -420,12 +430,7 @@ namespace OpenNest.Controls
|
||||
switch (keyData)
|
||||
{
|
||||
case Keys.Escape:
|
||||
if (currentAction.IsBusy())
|
||||
currentAction.CancelAction();
|
||||
else if (currentAction is ActionSelect && previousAction != null)
|
||||
RestorePreviousAction();
|
||||
else
|
||||
SetAction(typeof(ActionSelect));
|
||||
ProcessEscapeKey();
|
||||
break;
|
||||
|
||||
case Keys.Left:
|
||||
@@ -791,9 +796,11 @@ namespace OpenNest.Controls
|
||||
progressForm.UpdateProgress(p);
|
||||
|
||||
if (p.IsOverallBest)
|
||||
{
|
||||
progressForm.UpdatePreview(p.BestParts);
|
||||
SetActiveParts(p.BestParts);
|
||||
}
|
||||
|
||||
SetActiveParts(p.BestParts);
|
||||
ActiveWorkArea = p.ActiveWorkArea;
|
||||
});
|
||||
|
||||
@@ -837,6 +844,7 @@ namespace OpenNest.Controls
|
||||
ActiveWorkArea = null;
|
||||
progressForm.Close();
|
||||
cts.Dispose();
|
||||
Focus();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,17 @@ namespace OpenNest.Forms
|
||||
private const float ZoomInFactor = 1.5f;
|
||||
private const float ZoomOutFactor = 1.0f / ZoomInFactor;
|
||||
|
||||
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
|
||||
{
|
||||
if (keyData == Keys.Escape && activeForm?.PlateView != null)
|
||||
{
|
||||
activeForm.PlateView.ProcessEscapeKey();
|
||||
return true;
|
||||
}
|
||||
|
||||
return base.ProcessCmdKey(ref msg, keyData);
|
||||
}
|
||||
|
||||
public MainForm()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
Reference in New Issue
Block a user