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(
|
var bestFits = BestFitCache.GetOrCompute(
|
||||||
drawing, Plate.Size.Length, Plate.Size.Width, Plate.PartSpacing);
|
drawing, Plate.Size.Length, Plate.Size.Width, Plate.PartSpacing);
|
||||||
|
|
||||||
var best = bestFits.FirstOrDefault(r => r.Keep);
|
var best = SelectBestFitPair(bestFits);
|
||||||
if (best == null)
|
if (best == null)
|
||||||
return 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();
|
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;
|
var offset = workArea.Location - bbox.Location;
|
||||||
foreach (var p in parts)
|
foreach (var p in parts)
|
||||||
{
|
{
|
||||||
p.Offset(offset);
|
p.Offset(offset);
|
||||||
p.UpdateBounds();
|
p.UpdateBounds();
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -309,14 +344,18 @@ namespace OpenNest
|
|||||||
// during progress reporting.
|
// during progress reporting.
|
||||||
PhaseResults.Add(phaseResult);
|
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.CurrentBest = result;
|
||||||
context.CurrentBestScore = FillScore.Compute(result, context.WorkArea);
|
context.CurrentBestScore = FillScore.Compute(result, context.WorkArea);
|
||||||
context.WinnerPhase = strategy.Phase;
|
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
|
ReportProgress(context.Progress, new ProgressReport
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ namespace OpenNest.Engine.Fill
|
|||||||
// rectangular obstacle boundary. Without this, gaps between
|
// rectangular obstacle boundary. Without this, gaps between
|
||||||
// individual bounding boxes cause the next drawing to fill
|
// individual bounding boxes cause the next drawing to fill
|
||||||
// into inter-row spaces, producing an interleaved layout.
|
// into inter-row spaces, producing an interleaved layout.
|
||||||
if (placed.Count > 1)
|
if (placed.Count > 2)
|
||||||
RemoveTopmostPart(placed);
|
RemoveTopmostPart(placed);
|
||||||
|
|
||||||
allParts.AddRange(placed);
|
allParts.AddRange(placed);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using OpenNest.Engine;
|
using OpenNest.Engine;
|
||||||
|
using OpenNest.Engine.BestFit;
|
||||||
using OpenNest.Engine.Fill;
|
using OpenNest.Engine.Fill;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
using OpenNest.Math;
|
using OpenNest.Math;
|
||||||
@@ -26,6 +27,20 @@ namespace OpenNest
|
|||||||
|
|
||||||
public override ShrinkAxis TrimAxis => ShrinkAxis.Length;
|
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)
|
public override List<double> BuildAngles(NestItem item, ClassificationResult classification, Box workArea)
|
||||||
{
|
{
|
||||||
var baseAngles = new List<double> { classification.PrimaryAngle, classification.PrimaryAngle + Angle.HalfPI };
|
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 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) ---
|
// --- Virtual methods (side-effect-free, return parts) ---
|
||||||
|
|
||||||
public virtual List<Part> Fill(NestItem item, Box workArea,
|
public virtual List<Part> Fill(NestItem item, Box workArea,
|
||||||
@@ -333,7 +338,7 @@ namespace OpenNest
|
|||||||
|
|
||||||
var bestFits = BestFitCache.GetOrCompute(
|
var bestFits = BestFitCache.GetOrCompute(
|
||||||
item.Drawing, Plate.Size.Length, Plate.Size.Width, Plate.PartSpacing);
|
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;
|
if (bestFit == null) continue;
|
||||||
|
|
||||||
var parts = bestFit.BuildParts(item.Drawing);
|
var parts = bestFit.BuildParts(item.Drawing);
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ public class ColumnFillStrategy : IFillStrategy
|
|||||||
|
|
||||||
public List<Part> Fill(FillContext context)
|
public List<Part> Fill(FillContext context)
|
||||||
{
|
{
|
||||||
|
if (context.PartType == PartType.Rectangle)
|
||||||
|
return null;
|
||||||
|
|
||||||
var filler = new StripeFiller(context, NestDirection.Vertical) { CompleteStripesOnly = true };
|
var filler = new StripeFiller(context, NestDirection.Vertical) { CompleteStripesOnly = true };
|
||||||
return filler.Fill();
|
return filler.Fill();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,16 +32,29 @@ namespace OpenNest.Engine.Strategies
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Standard progress reporting for strategies and fillers. Reports intermediate
|
/// Standard progress reporting for strategies and fillers. Reports intermediate
|
||||||
/// results using the current ActivePhase, PlateNumber, and WorkArea.
|
/// 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>
|
/// </summary>
|
||||||
public void ReportProgress(List<Part> parts, string description)
|
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
|
NestEngineBase.ReportProgress(Progress, new ProgressReport
|
||||||
{
|
{
|
||||||
Phase = ActivePhase,
|
Phase = ActivePhase,
|
||||||
PlateNumber = PlateNumber,
|
PlateNumber = PlateNumber,
|
||||||
Parts = parts,
|
Parts = isNewBest ? parts : CurrentBest,
|
||||||
WorkArea = WorkArea,
|
WorkArea = WorkArea,
|
||||||
Description = description,
|
Description = description,
|
||||||
|
IsOverallBest = isNewBest,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ public class RowFillStrategy : IFillStrategy
|
|||||||
|
|
||||||
public List<Part> Fill(FillContext context)
|
public List<Part> Fill(FillContext context)
|
||||||
{
|
{
|
||||||
|
if (context.PartType == PartType.Rectangle)
|
||||||
|
return null;
|
||||||
|
|
||||||
var filler = new StripeFiller(context, NestDirection.Horizontal) { CompleteStripesOnly = true };
|
var filler = new StripeFiller(context, NestDirection.Horizontal) { CompleteStripesOnly = true };
|
||||||
return filler.Fill();
|
return filler.Fill();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using OpenNest.Engine;
|
using OpenNest.Engine;
|
||||||
|
using OpenNest.Engine.BestFit;
|
||||||
using OpenNest.Engine.Fill;
|
using OpenNest.Engine.Fill;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
using OpenNest.Math;
|
using OpenNest.Math;
|
||||||
@@ -24,6 +25,20 @@ namespace OpenNest
|
|||||||
|
|
||||||
public override NestDirection? PreferredDirection => NestDirection.Horizontal;
|
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)
|
public override List<double> BuildAngles(NestItem item, ClassificationResult classification, Box workArea)
|
||||||
{
|
{
|
||||||
var baseAngles = new List<double> { classification.PrimaryAngle, classification.PrimaryAngle + Angle.HalfPI };
|
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)
|
protected override bool ProcessDialogKey(Keys keyData)
|
||||||
{
|
{
|
||||||
// Only handle TAB, RETURN, ESC, and ARROW KEYS here.
|
// Only handle TAB, RETURN, ESC, and ARROW KEYS here.
|
||||||
@@ -420,12 +430,7 @@ namespace OpenNest.Controls
|
|||||||
switch (keyData)
|
switch (keyData)
|
||||||
{
|
{
|
||||||
case Keys.Escape:
|
case Keys.Escape:
|
||||||
if (currentAction.IsBusy())
|
ProcessEscapeKey();
|
||||||
currentAction.CancelAction();
|
|
||||||
else if (currentAction is ActionSelect && previousAction != null)
|
|
||||||
RestorePreviousAction();
|
|
||||||
else
|
|
||||||
SetAction(typeof(ActionSelect));
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Keys.Left:
|
case Keys.Left:
|
||||||
@@ -791,9 +796,11 @@ namespace OpenNest.Controls
|
|||||||
progressForm.UpdateProgress(p);
|
progressForm.UpdateProgress(p);
|
||||||
|
|
||||||
if (p.IsOverallBest)
|
if (p.IsOverallBest)
|
||||||
|
{
|
||||||
progressForm.UpdatePreview(p.BestParts);
|
progressForm.UpdatePreview(p.BestParts);
|
||||||
|
|
||||||
SetActiveParts(p.BestParts);
|
SetActiveParts(p.BestParts);
|
||||||
|
}
|
||||||
|
|
||||||
ActiveWorkArea = p.ActiveWorkArea;
|
ActiveWorkArea = p.ActiveWorkArea;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -837,6 +844,7 @@ namespace OpenNest.Controls
|
|||||||
ActiveWorkArea = null;
|
ActiveWorkArea = null;
|
||||||
progressForm.Close();
|
progressForm.Close();
|
||||||
cts.Dispose();
|
cts.Dispose();
|
||||||
|
Focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,17 @@ namespace OpenNest.Forms
|
|||||||
private const float ZoomInFactor = 1.5f;
|
private const float ZoomInFactor = 1.5f;
|
||||||
private const float ZoomOutFactor = 1.0f / ZoomInFactor;
|
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()
|
public MainForm()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|||||||
Reference in New Issue
Block a user