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 <noreply@anthropic.com>
This commit is contained in:
ajandClaude Opus 5.5 committed 2026-10-18 23:56:38 -04:00
1 parent c997b2e588
commit d384e63068
9 files changed
+95 -7

No files matched your search

@@ -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)
+7
View File
@@ -112,6 +112,13 @@ namespace OpenNest.Engine
public string Description { get; set; }
public Box ActiveWorkArea { get; set; }
/// <summary>
/// The sheet <see cref="BestParts"/> 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.
/// </summary>
public Plate PreviewSheet { get; set; }
public bool IsOverallBest { get; set; }
public int BestPartCount => BestParts?.Count ?? 0;
@@ -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<NestPlateStock, Plate> sheets = new(ReferenceEqualityComparer.Instance);
private TimeSpan? last;
/// <summary>Called after each placement on a sheet trial; reports when the interval has passed.</summary>
@@ -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)",
}));
@@ -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> { 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> { part });
Assert.Same(plate, view.SheetPlate);
view.SetActiveParts(new List<Part> { part }, trial);
view.ClearPreviewParts();
Assert.Same(plate, view.SheetPlate);
view.SetActiveParts(new List<Part> { 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;
}
}
+3 -3
View File
@@ -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;
+7 -1
View File
@@ -197,6 +197,9 @@ namespace OpenNest.Controls
private List<LayoutPart> displayParts;
private Func<bool> displayIsCurrent;
internal Plate DisplayPlate => displayPlate ?? plate;
/// <summary>The sheet drawn under everything: a proposal's, a preview's trial sheet, or the plate's.</summary>
internal Plate SheetPlate => displayPlate ?? previewManager.Sheet ?? plate;
internal List<LayoutPart> DisplayLayoutParts => displayParts ?? parts;
internal bool HasProposalDisplay => displayPlate != null;
@@ -845,7 +848,10 @@ namespace OpenNest.Controls
Plate.Parts.Add(part);
}
public void SetActiveParts(List<Part> parts) => previewManager.SetActiveParts(parts);
/// <summary>Shows preview parts; <paramref name="sheet"/>, when given, is drawn in place of
/// the plate's sheet until the preview is cleared (a whole-job engine's trial stock).</summary>
public void SetActiveParts(List<Part> parts, Plate sheet = null) =>
previewManager.SetActiveParts(parts, sheet);
public void ClearPreviewParts() => previewManager.ClearPreviewParts();
+8 -1
View File
@@ -15,13 +15,17 @@ namespace OpenNest.Controls
public IReadOnlyList<LayoutPart> PreviewParts => activeParts;
/// <summary>The sheet the preview parts lie on when it is not the view's plate; null otherwise.</summary>
public Plate Sheet { get; private set; }
public Brush PreviewBrush => view.ColorScheme.ActivePreviewPartBrush;
public Pen PreviewPen => view.ColorScheme.ActivePreviewPartPen;
public void SetActiveParts(List<Part> parts)
public void SetActiveParts(List<Part> 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;
}
}
}
+15 -1
View File
@@ -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<NestProgress>(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);
+4 -1
View File
@@ -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.