From d384e63068b62b8a5d6e5c228d48667ded334127 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Sun, 18 Oct 2026 22:55:04 -0400 Subject: [PATCH] feat(desktop): draw Auto Nest previews on the trial sheet NestProgress gains PreviewSheet, a detached plate for the stock the preview parts lie on; Irregular builds one per stock. PlateView draws it (size, edge spacing, clamps) in place of the plate while the preview is shown and drops it whenever the preview is cleared or the plate changes. Auto Nest zooms out when a trial sheet falls outside what it has already shown. Co-Authored-By: Claude Opus 5.5 --- .../IrregularNestingEngineTests.cs | 2 + OpenNest.Engine/NestProgress.cs | 7 +++ .../NestingEngines/Irregular/SheetPreview.cs | 6 +++ .../Controls/PlateViewPreviewSheetTests.cs | 43 +++++++++++++++++++ OpenNest/Controls/PlateRenderer.cs | 6 +-- OpenNest/Controls/PlateView.cs | 8 +++- OpenNest/Controls/PreviewManager.cs | 9 +++- OpenNest/Forms/Commands/AutoNestCommand.cs | 16 ++++++- docs/nesting-engines.md | 5 ++- 9 files changed, 95 insertions(+), 7 deletions(-) create mode 100644 OpenNest.WinForms.Tests/Controls/PlateViewPreviewSheetTests.cs 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.