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.