diff --git a/OpenNest.Engine.Tests/NestingEngines/IrregularNestingEngineTests.cs b/OpenNest.Engine.Tests/NestingEngines/IrregularNestingEngineTests.cs
index 664aa6f2..237c5d28 100644
--- a/OpenNest.Engine.Tests/NestingEngines/IrregularNestingEngineTests.cs
+++ b/OpenNest.Engine.Tests/NestingEngines/IrregularNestingEngineTests.cs
@@ -227,6 +227,8 @@ public class IrregularNestingEngineTests
foreach (var preview in previews.Select(r => r.LegacyProgress))
{
Assert.True(preview.IsOverallBest);
+ Assert.Equal(job.Plates[0].Size, preview.PreviewSheet.Size);
+ Assert.Equal(job.Plates[0].Quadrant, preview.PreviewSheet.Quadrant);
Assert.Equal((work.Left, work.Bottom, work.Right, work.Top), (preview.ActiveWorkArea.Left,
preview.ActiveWorkArea.Bottom, preview.ActiveWorkArea.Right, preview.ActiveWorkArea.Top));
foreach (var part in preview.BestParts)
diff --git a/OpenNest.Engine/NestProgress.cs b/OpenNest.Engine/NestProgress.cs
index 68418d0e..83b56585 100644
--- a/OpenNest.Engine/NestProgress.cs
+++ b/OpenNest.Engine/NestProgress.cs
@@ -112,6 +112,13 @@ namespace OpenNest.Engine
public string Description { get; set; }
public Box ActiveWorkArea { get; set; }
+
+ ///
+ /// The sheet lie on when it is not the caller's plate (a whole-job
+ /// engine's trial stock); null means the caller's own plate. Detached and never mutated.
+ ///
+ public Plate PreviewSheet { get; set; }
+
public bool IsOverallBest { get; set; }
public int BestPartCount => BestParts?.Count ?? 0;
diff --git a/OpenNest.Engine/NestingEngines/Irregular/SheetPreview.cs b/OpenNest.Engine/NestingEngines/Irregular/SheetPreview.cs
index 80f5fce6..245537b1 100644
--- a/OpenNest.Engine/NestingEngines/Irregular/SheetPreview.cs
+++ b/OpenNest.Engine/NestingEngines/Irregular/SheetPreview.cs
@@ -3,6 +3,7 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using OpenNest.Engine.Jobs;
+using OpenNest.Engine.Jobs.Adapters;
using OpenNest.Geometry;
namespace OpenNest.Engine.NestingEngines.Irregular;
@@ -22,6 +23,7 @@ internal sealed class SheetPreview(
internal static readonly TimeSpan Interval = TimeSpan.FromMilliseconds(250);
private readonly Stopwatch clock = Stopwatch.StartNew();
+ private readonly Dictionary sheets = new(ReferenceEqualityComparer.Instance);
private TimeSpan? last;
/// Called after each placement on a sheet trial; reports when the interval has passed.
@@ -42,6 +44,9 @@ internal sealed class SheetPreview(
part.UpdateBounds();
parts.Add(part);
}
+ // One detached sheet per stock, shared by its reports; front ends only read it.
+ if (!sheets.TryGetValue(stock, out var sheet))
+ sheets[stock] = sheet = DrawingJobMapper.CreatePlate(stock);
progress.Report(new NestJobProgress(NestJobStage.EvaluatingCandidate, stock.Id, plateIndex, 0, 0,
new NestProgress
@@ -50,6 +55,7 @@ internal sealed class SheetPreview(
PlateNumber = plateIndex + 1,
BestParts = parts,
ActiveWorkArea = stock.WorkArea,
+ PreviewSheet = sheet,
IsOverallBest = true,
Description = $"Irregular: packing plate {plateIndex + 1} on stock {stock.Id} ({parts.Count} parts)",
}));
diff --git a/OpenNest.WinForms.Tests/Controls/PlateViewPreviewSheetTests.cs b/OpenNest.WinForms.Tests/Controls/PlateViewPreviewSheetTests.cs
new file mode 100644
index 00000000..68316218
--- /dev/null
+++ b/OpenNest.WinForms.Tests/Controls/PlateViewPreviewSheetTests.cs
@@ -0,0 +1,43 @@
+using OpenNest.Controls;
+using OpenNest.Geometry;
+
+namespace OpenNest.WinForms.Tests.Controls;
+
+public class PlateViewPreviewSheetTests
+{
+ [Fact]
+ public void PreviewSheetReplacesThePlateUntilThePreviewEnds() => StaTestThread.Run(() =>
+ {
+ var plate = new Plate(48, 96);
+ var trial = new Plate(60, 120);
+ using var view = new PlateView { Plate = plate };
+ var part = new Part(new Drawing("square", Square()), new Vector(1, 1));
+
+ view.SetActiveParts(new List { part }, trial);
+ Assert.Same(trial, view.SheetPlate);
+ Assert.Same(plate, view.DisplayPlate);
+
+ // A preview without a sheet (interactive fill) is drawn on the plate.
+ view.SetActiveParts(new List { part });
+ Assert.Same(plate, view.SheetPlate);
+
+ view.SetActiveParts(new List { part }, trial);
+ view.ClearPreviewParts();
+ Assert.Same(plate, view.SheetPlate);
+
+ view.SetActiveParts(new List { part }, trial);
+ view.Plate = new Plate(48, 96);
+ Assert.Same(view.Plate, view.SheetPlate);
+ }, TimeSpan.FromMinutes(1), "The STA test did not complete.");
+
+ private static CNC.Program Square()
+ {
+ var program = new CNC.Program();
+ program.MoveTo(0, 0);
+ program.LineTo(0, 10);
+ program.LineTo(10, 10);
+ program.LineTo(10, 0);
+ program.LineTo(0, 0);
+ return program;
+ }
+}
diff --git a/OpenNest/Controls/PlateRenderer.cs b/OpenNest/Controls/PlateRenderer.cs
index 0ac136a5..c094d5e5 100644
--- a/OpenNest/Controls/PlateRenderer.cs
+++ b/OpenNest/Controls/PlateRenderer.cs
@@ -21,7 +21,7 @@ namespace OpenNest.Controls
public void DrawPlate(Graphics g)
{
- var plate = view.DisplayPlate;
+ var plate = view.SheetPlate;
var plateRect = new RectangleF
{
Width = view.LengthWorldToGui(plate.Size.Length),
@@ -178,7 +178,7 @@ namespace OpenNest.Controls
public void DrawClampZones(Graphics g)
{
- var plate = view.DisplayPlate;
+ var plate = view.SheetPlate;
if (plate == null || plate.ClampZones.Count == 0)
return;
@@ -214,7 +214,7 @@ namespace OpenNest.Controls
public void DrawRemovedClamps(Graphics g)
{
- var plate = view.DisplayPlate;
+ var plate = view.SheetPlate;
if (plate?.ClampRail == null || plate.RemovedClamps.Count == 0)
return;
diff --git a/OpenNest/Controls/PlateView.cs b/OpenNest/Controls/PlateView.cs
index 324271e9..aa62de91 100644
--- a/OpenNest/Controls/PlateView.cs
+++ b/OpenNest/Controls/PlateView.cs
@@ -197,6 +197,9 @@ namespace OpenNest.Controls
private List displayParts;
private Func displayIsCurrent;
internal Plate DisplayPlate => displayPlate ?? plate;
+
+ /// The sheet drawn under everything: a proposal's, a preview's trial sheet, or the plate's.
+ internal Plate SheetPlate => displayPlate ?? previewManager.Sheet ?? plate;
internal List DisplayLayoutParts => displayParts ?? parts;
internal bool HasProposalDisplay => displayPlate != null;
@@ -845,7 +848,10 @@ namespace OpenNest.Controls
Plate.Parts.Add(part);
}
- public void SetActiveParts(List parts) => previewManager.SetActiveParts(parts);
+ /// Shows preview parts; , when given, is drawn in place of
+ /// the plate's sheet until the preview is cleared (a whole-job engine's trial stock).
+ public void SetActiveParts(List parts, Plate sheet = null) =>
+ previewManager.SetActiveParts(parts, sheet);
public void ClearPreviewParts() => previewManager.ClearPreviewParts();
diff --git a/OpenNest/Controls/PreviewManager.cs b/OpenNest/Controls/PreviewManager.cs
index 26e8acaa..74b57c3e 100644
--- a/OpenNest/Controls/PreviewManager.cs
+++ b/OpenNest/Controls/PreviewManager.cs
@@ -15,13 +15,17 @@ namespace OpenNest.Controls
public IReadOnlyList PreviewParts => activeParts;
+ /// The sheet the preview parts lie on when it is not the view's plate; null otherwise.
+ public Plate Sheet { get; private set; }
+
public Brush PreviewBrush => view.ColorScheme.ActivePreviewPartBrush;
public Pen PreviewPen => view.ColorScheme.ActivePreviewPartPen;
- public void SetActiveParts(List parts)
+ public void SetActiveParts(List parts, Plate sheet = null)
{
activeParts.Clear();
+ Sheet = sheet;
if (parts != null)
{
@@ -35,6 +39,7 @@ namespace OpenNest.Controls
public void ClearPreviewParts()
{
activeParts.Clear();
+ Sheet = null;
view.Invalidate();
}
@@ -47,6 +52,7 @@ namespace OpenNest.Controls
}
activeParts.Clear();
+ Sheet = null;
}
public void Update()
@@ -57,6 +63,7 @@ namespace OpenNest.Controls
public void Clear()
{
activeParts.Clear();
+ Sheet = null;
}
}
}
diff --git a/OpenNest/Forms/Commands/AutoNestCommand.cs b/OpenNest/Forms/Commands/AutoNestCommand.cs
index 36c3afa6..f7b74860 100644
--- a/OpenNest/Forms/Commands/AutoNestCommand.cs
+++ b/OpenNest/Forms/Commands/AutoNestCommand.cs
@@ -7,6 +7,7 @@ using System.Windows.Forms;
using OpenNest.Diagnostics;
using OpenNest.Engine;
using OpenNest.Engine.Jobs;
+using OpenNest.Geometry;
namespace OpenNest.Forms.Commands
{
@@ -79,13 +80,26 @@ namespace OpenNest.Forms.Commands
HoldOpenUntilCompleted = true,
};
var receivingProgress = true;
+ // Everything shown so far; the view zooms out only when a trial sheet falls outside it.
+ var shown = target.PlateView.Plate.BoundingBox(false);
var progress = new Progress(p =>
{
if (!receivingProgress || target.IsDisposed || cts.IsCancellationRequested)
return;
progressForm.UpdateProgress(p);
if (p.IsOverallBest)
- target.PlateView.SetActiveParts(p.BestParts);
+ {
+ target.PlateView.SetActiveParts(p.BestParts, p.PreviewSheet);
+ var sheet = p.PreviewSheet?.BoundingBox(false);
+ if (sheet != null && !shown.Contains(sheet))
+ {
+ var left = System.Math.Min(shown.Left, sheet.Left);
+ var bottom = System.Math.Min(shown.Bottom, sheet.Bottom);
+ shown = new Box(left, bottom, System.Math.Max(shown.Right, sheet.Right) - left,
+ System.Math.Max(shown.Top, sheet.Top) - bottom);
+ target.PlateView.ZoomToArea(shown);
+ }
+ }
target.PlateView.ActiveWorkArea = p.ActiveWorkArea;
});
var jobProgress = JobEngineNest.CreateProgress(EngineName, progress);
diff --git a/docs/nesting-engines.md b/docs/nesting-engines.md
index b1ad7b93..07b65e8b 100644
--- a/docs/nesting-engines.md
+++ b/docs/nesting-engines.md
@@ -65,7 +65,10 @@ work-area bounds remains invalid.
While it runs with a progress sink, Irregular previews the sheet it is packing: at most four
times a second it reports the placements so far as `NestProgress.BestParts` with the stock's
-work area, and desktop Auto Nest draws them on the plate view. The preview follows every trial
+work area and a detached `PreviewSheet` built from the stock. Desktop Auto Nest draws the parts
+on that sheet (its size, edge spacing and clamps) in place of the current plate's, zooms out when
+a trial sheet falls outside what it has shown, and returns to the plate when the run ends. The
+preview follows every trial
(each stock, sheet, strategy variant and tail re-plan), so it shows the search at work, not the
layout that will be kept. Reporting only reads finished placements, so layouts are identical
with or without a sink.