feat(desktop): nest released drawing sets with Auto Nest before saving

A release now ends in nests ready to cut rather than empty drafts, and a
merge no longer leaves the draft it cleared waiting for someone to run Auto
Nest. "Nest after release" (on by default) runs Auto Nest headless on every
released group, new or merged, before anything is uploaded.

NestRenest removes every plate and nests each drawing's required quantity
onto sheets from the nest's plate defaults and plate-size options. It keeps
a layout only when it is valid and keepable, so an invalid result never
reaches the server; the nest is saved without sheets and the operator is
told to run Auto Nest. Unplaced copies stay as remaining quantity, and the
status column lists sheets, utilization and what did not fit. A new Nesting
progress stage reports each group, and cancelling while nesting saves
nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ajandClaude Opus 5.5 committed 2026-10-21 10:25:55 -04:00
1 parent f6781d27c3
commit 3ca7d6185f
9 files changed
+402 -26

No files matched your search

+10 -6
View File
@@ -62,12 +62,12 @@ public sealed record SetReleaseRenest(bool Nested, string Note);
public enum SetReleaseStage
{
/// <summary>
/// Downloading the group's drawings (and draft), building its nest and nesting it when
/// asked; nothing is uploaded yet.
/// </summary>
/// <summary>Downloading the group's drawings (and draft) and building its nest; nothing is uploaded yet.</summary>
Preparing,
/// <summary>Nesting the group's built nest after release; nothing is uploaded yet.</summary>
Nesting,
/// <summary>Uploading the group's nest, or saving the draft it merged into.</summary>
Uploading,
@@ -242,11 +242,13 @@ public sealed class SetReleaseSession
for (var i = 0; i < results.Count; i++)
{
var result = results[i];
var index = i;
progress?.Report(new SetReleaseProgress(SetReleaseStage.Preparing, i, results.Count, result.Group));
bool ready;
try
{
ready = await PrepareAsync(plan, result, cancellationToken);
ready = await PrepareAsync(plan, result, () => progress?.Report(
new SetReleaseProgress(SetReleaseStage.Nesting, index, results.Count, result.Group)), cancellationToken);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
@@ -341,7 +343,8 @@ public sealed class SetReleaseSession
// Builds the group's nest in memory, new or merged, and nests it when asked; false when
// the group is refused.
private async Task<bool> PrepareAsync(SetReleasePlan plan, SetReleaseGroupResult result, CancellationToken cancellationToken)
private async Task<bool> PrepareAsync(SetReleasePlan plan, SetReleaseGroupResult result, Action nesting,
CancellationToken cancellationToken)
{
var group = result.Group;
Nest nest;
@@ -405,6 +408,7 @@ public sealed class SetReleaseSession
if (_renest != null)
{
nesting();
try
{
result.Renest = await _renest(nest, cancellationToken)
+98
View File
@@ -0,0 +1,98 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
namespace OpenNest.Engine.Jobs;
/// <summary>
/// What <see cref="NestRenest"/> kept: whether a layout was committed, its physical sheets
/// and utilization, the copies it left unplaced, and one operator-facing note.
/// </summary>
public sealed record NestRenestResult(
bool Nested,
int Sheets,
double Utilization,
IReadOnlyList<NestUnplacedPart> Unplaced,
string Note
);
/// <summary>
/// Auto Nest without the dialog, for a nest no one is looking at yet (a released drawing
/// set): every plate is removed, then every drawing's required quantity is nested onto
/// sheets from the nest's plate defaults and plate-size options through
/// <see cref="NestPipeline"/>. A layout is committed only when it is valid and keepable;
/// otherwise the nest is left without plates for the operator to run Auto Nest. Copies the
/// layout leaves unplaced stay as remaining quantity. The caller owns the nest throughout.
/// </summary>
public static class NestRenest
{
/// <summary>The note left when no layout is kept.</summary>
public const string NotKeptNote = "Auto Nest could not keep a layout: open it and run Auto Nest.";
/// <summary>Most physical sheets one renest may use, as the Auto Nest dialog allows.</summary>
public const int MaxPlates = 100;
/// <summary>Renests with the engine registered as <paramref name="engineName"/>.</summary>
public static NestRenestResult Run(Nest nest, string engineName, double minimumSalvageDimension = 0,
IProgress<NestJobProgress> progress = null, CancellationToken token = default) =>
Run(nest, NestingEngineRegistry.Create(engineName), engineName, minimumSalvageDimension, progress, token);
/// <param name="minimumSalvageDimension">
/// Smallest offcut credited when the nest offers plate-size options, in the nest's units.
/// </param>
public static NestRenestResult Run(Nest nest, INestingEngine engine, string engineName,
double minimumSalvageDimension = 0, IProgress<NestJobProgress> progress = null, CancellationToken token = default)
{
ArgumentNullException.ThrowIfNull(nest);
ArgumentNullException.ThrowIfNull(engine);
token.ThrowIfCancellationRequested();
nest.Plates.Clear();
nest.UpdateDrawingQuantities();
var items = nest.Drawings
.Where(d => !d.IsCutOff && d.Quantity.Required > 0)
.Select(d => new NestItem
{
Drawing = d,
Quantity = d.Quantity.Required,
Priority = d.Priority,
RotationStart = d.Constraints.StartAngle,
RotationEnd = d.Constraints.EndAngle,
StepAngle = d.Constraints.StepAngle,
})
.ToList();
if (items.Count == 0)
return new NestRenestResult(false, 0, 0, Array.Empty<NestUnplacedPart>(), "Nothing has a quantity to nest.");
var options = nest.PlateOptions;
var request = new NestPipelineRequest(engineName, items,
NestStockBuilder.FromTemplate(nest.PlateDefaults.CreateNew(), options),
new NestJobOptions(maxPlates: MaxPlates,
salvageRate: options.Count > 0 ? nest.SalvageRate : 0,
minimumSalvageDimension: options.Count > 0 ? minimumSalvageDimension : 0));
var result = NestPipeline.Run(engine, engineName, request, progress, token);
// Engines that ignore cancellation still have their proposal discarded.
token.ThrowIfCancellationRequested();
if (!result.IsValid || !result.CanKeep || result.Plates.All(p => p.Parts.Count == 0))
return new NestRenestResult(false, 0, 0, result.Unplaced, NotKeptNote);
var applied = NestPipelineCommit.ApplyToPlates(result, nest.CreatePlate, token: token);
var sheets = applied.Sum(p => p.Quantity);
var area = applied.Sum(p => p.Area() * p.Quantity);
var utilization = area > 0 ? applied.Sum(p => p.Utilization() * p.Area() * p.Quantity) / area : 0;
return new NestRenestResult(true, sheets, utilization, result.Unplaced, Note(sheets, utilization, result.Unplaced));
}
private static string Note(int sheets, double utilization, IReadOnlyList<NestUnplacedPart> unplaced)
{
var note = string.Format(CultureInfo.CurrentCulture, "Nested on {0} sheet{1}, {2:P0} used.",
sheets, sheets == 1 ? "" : "s", utilization);
if (unplaced.Count == 0)
return note;
var copies = unplaced.Sum(u => u.Count);
return note + $" {copies} cop{(copies == 1 ? "y" : "ies")} not placed, left to nest: "
+ string.Join("; ", unplaced.Select(u => u.Describe())) + ".";
}
}
@@ -464,6 +464,29 @@ public class SetReleaseSessionTests
Assert.Single(_sets.Posted);
}
[Fact]
public async Task Run_CanceledWhileNestingAfterRelease_UploadsNothing()
{
var plan = SetReleasePlan.Build(Set((Stored("SYN-A"), 1), (Stored("SYN-B", cutType: CutType.Plasma), 1)), 1, false);
using var cancel = new CancellationTokenSource();
var progress = new RecordingProgress();
var session = Session(renest: (_, token) =>
{
cancel.Cancel();
token.ThrowIfCancellationRequested();
return Task.FromResult(new SetReleaseRenest(true, ""));
});
var result = await session.RunAsync(plan, plan.Groups, "", "", progress, cancel.Token);
Assert.True(result.Canceled);
Assert.Equal(new[] { (SetReleaseStage.Preparing, 0), (SetReleaseStage.Nesting, 0) },
progress.Reports.Select(p => (p.Stage, p.Index)));
Assert.All(result.Groups, g => Assert.Equal(SetReleaseGroupOutcome.NotStarted, g.Outcome));
Assert.Empty(_nests.Uploaded);
Assert.Empty(_sets.Posted);
}
[Fact]
public async Task FindDrafts_AsksForDraftsOfTheGroup()
{
+99
View File
@@ -0,0 +1,99 @@
using OpenNest.Engine;
using OpenNest.Engine.Jobs;
using OpenNest.Geometry;
using OpenNest.Shapes;
namespace OpenNest.Tests.Engine;
public class NestRenestTests
{
private static Nest NestOf(params (string Name, double Length, double Width, int Quantity)[] drawings)
{
var nest = new Nest("N26-0001");
nest.PlateDefaults.Size = new Size(48, 96);
foreach (var (name, length, width, quantity) in drawings)
{
var drawing = new RectangleShape { Name = name, Length = length, Width = width }.GetDrawing();
drawing.Quantity.Required = quantity;
nest.Drawings.Add(drawing);
}
return nest;
}
[Fact]
public void Run_DiscardsTheOldLayout_AndNestsTheWholeDemandFromThePlateDefaults()
{
var nest = NestOf(("SYN-A", 4, 2, 6), ("SYN-B", 3, 3, 2));
var old = nest.CreatePlate();
old.Size = new Size(10, 10);
old.Parts.Add(new Part(nest.Drawings.First()));
nest.CreatePlate();
var result = NestRenest.Run(nest, "Rectangles");
Assert.True(result.Nested, result.Note);
Assert.DoesNotContain(old, nest.Plates);
var plate = Assert.Single(nest.Plates);
Assert.Equal(new Size(48, 96), plate.Size);
Assert.Equal(8, plate.Parts.Count);
Assert.All(nest.Drawings, d => Assert.Equal((d.Quantity.Required, 0), (d.Quantity.Nested, d.Quantity.Remaining)));
Assert.Equal((1, plate.Utilization()), (result.Sheets, result.Utilization));
Assert.Empty(result.Unplaced);
Assert.StartsWith("Nested on 1 sheet, ", result.Note);
}
[Fact]
public void Run_LeavesCopiesThatDoNotFit_AsRemainingQuantity_AndListsThem()
{
var nest = NestOf(("SYN-SMALL", 4, 2, 2), ("SYN-HUGE", 200, 200, 1));
var result = NestRenest.Run(nest, "Rectangles");
Assert.True(result.Nested, result.Note);
Assert.Equal(2, nest.Plates.Sum(p => p.Parts.Count));
var huge = nest.Drawings.Single(d => d.Name == "SYN-HUGE");
Assert.Equal(1, huge.Quantity.Remaining);
Assert.Same(huge, Assert.Single(result.Unplaced).Drawing);
Assert.Contains(" 1 copy not placed, left to nest: ", result.Note);
Assert.Contains("SYN-HUGE: 1 not placed", result.Note);
}
[Fact]
public void Run_WhenTheLayoutIsInvalid_LeavesTheNestWithoutPlates()
{
var nest = NestOf(("SYN-A", 4, 2, 2));
var old = nest.CreatePlate();
old.Parts.Add(new Part(nest.Drawings.First()));
var result = NestRenest.Run(nest, new OverlappingEngine(), "overlapping");
Assert.False(result.Nested);
Assert.Equal(NestRenest.NotKeptNote, result.Note);
Assert.Empty(nest.Plates);
Assert.Equal(2, nest.Drawings.First().Quantity.Remaining);
}
[Fact]
public void Run_WhenCanceled_KeepsNothing()
{
var nest = NestOf(("SYN-A", 4, 2, 2));
using var canceled = new CancellationTokenSource();
canceled.Cancel();
Assert.Throws<OperationCanceledException>(() => NestRenest.Run(nest, "Rectangles", token: canceled.Token));
Assert.Empty(nest.Plates);
}
/// <summary>Puts every copy at the same spot, so the layout never validates.</summary>
private sealed class OverlappingEngine : INestingEngine
{
public NestJobResult Solve(NestJob job, IProgress<NestJobProgress>? progress = null,
CancellationToken token = default)
{
var builder = new NestJobResultBuilder(job);
builder.AddSheet(job.Plates[0], Enumerable.Range(0, job.Parts[0].Quantity)
.Select(_ => (job.Parts[0].Id, 5.0, 5.0, 0.0)).ToArray());
return builder.Build(NestJobStopReason.Completed);
}
}
}
@@ -254,6 +254,86 @@ public class SetReleaseFormsTests
"The multi-set merge release test did not complete.");
}
[Fact]
public void NestAfterRelease_NestsEachGroupWithAutoNest_AndListsItsSheets()
{
StaTestThread.Run(
() =>
{
var library = new PackageLibrary();
var a = library.Add("SYN-A", "Synthetic Steel");
var sets = new InMemoryDrawingSets();
var set = CreateSet(sets, (a, 3));
var opened = new List<Nest>();
var questions = new List<string>();
using var form = new ReleaseSetForm(sets, library, new MemoryNests(), Server, set, (nest, _) => opened.Add(nest));
form.ShowMessage = (text, _, _) => { questions.Add(text); return DialogResult.OK; };
form.CreateNest = (group, _) =>
{
var nest = new Nest("N26-0200") { Units = group.Units };
nest.PlateDefaults.Size = new OpenNest.Geometry.Size(48, 96);
return Task.FromResult(nest);
};
form.Show();
Assert.True(Field<CheckBox>(form, "nestAfterReleaseCheckBox").Checked);
Run(form.ReleaseAsync());
Assert.Contains("Each is nested with Auto Nest", Assert.Single(questions));
var nest = Assert.Single(opened);
Assert.Equal(3, nest.Plates.Sum(p => p.Parts.Count * p.Quantity));
Assert.Equal(0, Assert.Single(nest.Drawings).Quantity.Remaining);
var status = (string)Field<DataGridView>(form, "groupGrid").Rows[0].Cells["statusColumn"].Value;
Assert.StartsWith("Created N26-0200 — Nested on 1 sheet, ", status);
},
TestTimeout,
"The nest after release test did not complete.");
}
[Fact]
public void NestAfterRelease_Unticked_LeavesNestsAndMergedDraftsForAutoNest()
{
StaTestThread.Run(
() =>
{
var library = new PackageLibrary();
var a = library.Add("SYN-A", "Synthetic Steel");
var sets = new InMemoryDrawingSets();
var set = CreateSet(sets, (a, 2));
var nests = new MemoryNests();
var draft = nests.AddDraft("N26-0007", "Synthetic Steel", "Grade A", 0.25);
var questions = new List<string>();
var renested = 0;
using var form = new ReleaseSetForm(sets, library, nests, Server, set, (_, _) => { });
form.ShowMessage = (text, _, _) => { questions.Add(text); return DialogResult.OK; };
form.CreateNest = (group, _) => Task.FromResult(new Nest("N") { Units = group.Units });
form.Renest = (_, _) => { renested++; return Task.FromResult(new SetReleaseRenest(true, "")); };
form.Show();
var row = Field<DataGridView>(form, "groupGrid").Rows[0];
PumpUntil(() => ((DataGridViewComboBoxCell)row.Cells["targetColumn"]).Items.Count == 2, "the drafts to merge into");
row.Cells["targetColumn"].Value = ((DataGridViewComboBoxCell)row.Cells["targetColumn"]).Items[1];
Assert.Contains("and nests everything again", (string)row.Cells["statusColumn"].Value);
Field<CheckBox>(form, "nestAfterReleaseCheckBox").Checked = false;
Assert.Contains("and leaves everything for Auto Nest", (string)row.Cells["statusColumn"].Value);
Run(form.ReleaseAsync());
Assert.Equal(0, renested);
Assert.Contains("current layout, left for Auto Nest", Assert.Single(questions));
Assert.DoesNotContain("nested with Auto Nest", questions[0]);
var merged = Assert.Single(form.Result!.Created);
Assert.Same(draft, merged.MergedInto);
Assert.Empty(merged.Nest!.Plates);
Assert.Null(merged.Renest);
Assert.False(Field<CheckBox>(form, "nestAfterReleaseCheckBox").Enabled);
},
TestTimeout,
"The release without nesting test did not complete.");
}
[Fact]
public void Releases_UndoOffersToLeaveChangedNests_AndShowsWhatTheReleaseDid()
{
+4 -1
View File
@@ -56,13 +56,16 @@ namespace OpenNest.Forms
set { salvageRateBox.Text = (value * 100).ToString("F0"); }
}
/// <summary>The smallest offcut credited as salvage unless the operator types another.</summary>
public const double DefaultMinRemnantSize = 12.0;
public double MinRemnantSize
{
get
{
if (double.TryParse(minRemnantBox.Text, out var val) && val > 0)
return val;
return 12.0;
return DefaultMinRemnantSize;
}
}
+22 -5
View File
@@ -36,6 +36,7 @@ namespace OpenNest.Forms
kitsColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
optionsTable = new System.Windows.Forms.TableLayoutPanel();
includeArchivedCheckBox = new System.Windows.Forms.CheckBox();
nestAfterReleaseCheckBox = new System.Windows.Forms.CheckBox();
notesLabel = new System.Windows.Forms.Label();
notesBox = new System.Windows.Forms.TextBox();
groupGrid = new System.Windows.Forms.DataGridView();
@@ -211,13 +212,15 @@ namespace OpenNest.Forms
// optionsTable
//
optionsTable.AutoSize = true;
optionsTable.ColumnCount = 3;
optionsTable.ColumnCount = 4;
optionsTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle());
optionsTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle());
optionsTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle());
optionsTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F));
optionsTable.Controls.Add(includeArchivedCheckBox, 0, 0);
optionsTable.Controls.Add(notesLabel, 1, 0);
optionsTable.Controls.Add(notesBox, 2, 0);
optionsTable.Controls.Add(nestAfterReleaseCheckBox, 1, 0);
optionsTable.Controls.Add(notesLabel, 2, 0);
optionsTable.Controls.Add(notesBox, 3, 0);
optionsTable.Dock = System.Windows.Forms.DockStyle.Fill;
optionsTable.Margin = new System.Windows.Forms.Padding(0, 0, 0, 6);
optionsTable.Name = "optionsTable";
@@ -235,13 +238,26 @@ namespace OpenNest.Forms
includeArchivedCheckBox.UseVisualStyleBackColor = true;
includeArchivedCheckBox.CheckedChanged += Options_Changed;
//
// nestAfterReleaseCheckBox
//
nestAfterReleaseCheckBox.Anchor = System.Windows.Forms.AnchorStyles.Left;
nestAfterReleaseCheckBox.AutoSize = true;
nestAfterReleaseCheckBox.Checked = true;
nestAfterReleaseCheckBox.CheckState = System.Windows.Forms.CheckState.Checked;
nestAfterReleaseCheckBox.Margin = new System.Windows.Forms.Padding(12, 3, 3, 3);
nestAfterReleaseCheckBox.Name = "nestAfterReleaseCheckBox";
nestAfterReleaseCheckBox.TabIndex = 1;
nestAfterReleaseCheckBox.Text = "Nest after release";
nestAfterReleaseCheckBox.UseVisualStyleBackColor = true;
nestAfterReleaseCheckBox.CheckedChanged += NestAfterReleaseCheckBox_CheckedChanged;
//
// notesLabel
//
notesLabel.Anchor = System.Windows.Forms.AnchorStyles.Left;
notesLabel.AutoSize = true;
notesLabel.Margin = new System.Windows.Forms.Padding(12, 0, 3, 0);
notesLabel.Name = "notesLabel";
notesLabel.TabIndex = 1;
notesLabel.TabIndex = 2;
notesLabel.Text = "Release notes:";
//
// notesBox
@@ -249,7 +265,7 @@ namespace OpenNest.Forms
notesBox.Dock = System.Windows.Forms.DockStyle.Fill;
notesBox.MaxLength = 4000;
notesBox.Name = "notesBox";
notesBox.TabIndex = 2;
notesBox.TabIndex = 3;
//
// groupGrid
//
@@ -462,6 +478,7 @@ namespace OpenNest.Forms
private System.Windows.Forms.DataGridViewTextBoxColumn kitsColumn;
private System.Windows.Forms.TableLayoutPanel optionsTable;
private System.Windows.Forms.CheckBox includeArchivedCheckBox;
private System.Windows.Forms.CheckBox nestAfterReleaseCheckBox;
private System.Windows.Forms.Label notesLabel;
private System.Windows.Forms.TextBox notesBox;
private System.Windows.Forms.DataGridView groupGrid;
+48 -8
View File
@@ -10,6 +10,7 @@ using System.Windows.Forms;
using OpenNest.Controls;
using OpenNest.Data;
using OpenNest.Engine.BestFit;
using OpenNest.Engine.Jobs;
using OpenNest.IO;
namespace OpenNest.Forms
@@ -19,10 +20,10 @@ namespace OpenNest.Forms
/// the browser, each with a number of kits, and the dialog previews how they split into one
/// nest per material group (<see cref="SetReleasePlan"/>), with an archived-drawing toggle
/// and a tick per group. Each group makes a new Draft nest or merges into a Draft nest of
/// the same material, which is renested. <see cref="SetReleaseSession"/> then builds and
/// saves those nests, opens each in its own tab, and records the release once every
/// chosen nest exists. The dialog shows every group's outcome; a release runs once per
/// dialog.
/// the same material, whose layout is discarded. <see cref="SetReleaseSession"/> then builds
/// those nests, nests each one with Auto Nest when Nest after release is ticked, saves them,
/// opens each in its own tab, and records the release once every chosen nest exists. The
/// dialog shows every group's outcome; a release runs once per dialog.
/// </summary>
public partial class ReleaseSetForm : Form
{
@@ -52,6 +53,12 @@ namespace OpenNest.Forms
/// <summary>Makes the empty nest for a group, as File &gt; New would in its units; replaced by tests.</summary>
internal Func<SetReleaseGroup, CancellationToken, Task<Nest>> CreateNest { get; set; }
/// <summary>
/// Nests a group's built nest when Nest after release is ticked: Auto Nest headless with
/// the selected engine (<see cref="NestRenest"/>); replaced by tests.
/// </summary>
internal Func<Nest, CancellationToken, Task<SetReleaseRenest>> Renest { get; set; } = RenestAsync;
/// <summary>
/// True when a server nest is open in a tab of this desktop. Such a draft is not merged
/// into: the tab could hold edits the merge would leave behind.
@@ -125,6 +132,20 @@ namespace OpenNest.Forms
BuildPlan();
}
// The merge notes say what happens to a draft's layout, which this choice changes.
private void NestAfterReleaseCheckBox_CheckedChanged(object sender, EventArgs e)
{
if (running || Result != null)
return;
populating = true;
foreach (DataGridViewRow row in groupGrid.Rows)
{
if (row.Tag is SetReleaseGroup group)
row.Cells[statusColumn.Index].Value = GroupNote(group);
}
populating = false;
}
private void SetGrid_CellValidating(object sender, DataGridViewCellValidatingEventArgs e)
{
if (e.ColumnIndex != kitsColumn.Index)
@@ -381,17 +402,21 @@ namespace OpenNest.Forms
: $"{lines.Count} sets";
var created = chosen.Count - merges.Count;
var text = $"Release {setsText} into {chosen.Count} Draft nest{(chosen.Count == 1 ? "" : "s")}";
var nestAfter = nestAfterReleaseCheckBox.Checked;
if (merges.Count > 0)
text += $": {created} new and {merges.Count} merged into existing drafts. A merged draft loses its "
+ "current layout and is nested again; Undo Release restores it";
if (ShowMessage(text + ". Each opens in its own tab.", MessageBoxButtons.OKCancel, MessageBoxIcon.Question) != DialogResult.OK)
+ (nestAfter ? "current layout and is nested again" : "current layout, left for Auto Nest")
+ "; Undo Release restores it";
text += nestAfter ? ". Each is nested with Auto Nest and opens in its own tab." : ". Each opens in its own tab.";
if (ShowMessage(text, MessageBoxButtons.OKCancel, MessageBoxIcon.Question) != DialogResult.OK)
return;
running = true;
cancellation = new CancellationTokenSource();
closeButton.Text = "Cancel";
UpdateCommands();
session = new SetReleaseSession(sets, library, nests, serverUrl, ReadPackage, WriteNest, CreateNest, ReadNest);
session = new SetReleaseSession(sets, library, nests, serverUrl, ReadPackage, WriteNest, CreateNest, ReadNest,
nestAfter ? Renest : null);
var progress = new Progress<SetReleaseProgress>(ShowProgress);
try
{
@@ -545,7 +570,9 @@ namespace OpenNest.Forms
var notes = group.Warnings.ToList();
if (targets.TryGetValue(group.Key, out var draft))
notes.Insert(0, $"Merging discards the layout of '{draft.Name}' ({draft.PlateCount} sheet"
+ $"{(draft.PlateCount == 1 ? "" : "s")}) and nests everything again; Undo Release restores it.");
+ $"{(draft.PlateCount == 1 ? "" : "s")}) and "
+ (nestAfterReleaseCheckBox.Checked ? "nests everything again" : "leaves everything for Auto Nest")
+ "; Undo Release restores it.");
return string.Join(" ", notes);
}
@@ -561,6 +588,7 @@ namespace OpenNest.Forms
statusLabel.Text = progress.Stage switch
{
SetReleaseStage.Preparing => $"Preparing nest {progress.Index + 1} of {progress.Count}: {Describe(progress.Group)}...",
SetReleaseStage.Nesting => $"Nesting nest {progress.Index + 1} of {progress.Count}: {Describe(progress.Group)}...",
SetReleaseStage.Uploading => $"Saving nest {progress.Index + 1} of {progress.Count}: {Describe(progress.Group)}...",
_ => "Recording the release...",
};
@@ -608,6 +636,7 @@ namespace OpenNest.Forms
addSetButton.Enabled = editable;
removeSetButton.Enabled = editable && setGrid.Rows.Count > 1 && setGrid.CurrentRow != null;
includeArchivedCheckBox.Enabled = editable;
nestAfterReleaseCheckBox.Enabled = editable;
notesBox.ReadOnly = !editable;
includeColumn.ReadOnly = !editable;
targetColumn.ReadOnly = !editable;
@@ -642,6 +671,17 @@ namespace OpenNest.Forms
$"{group.Thickness.ToString("0.####", CultureInfo.CurrentCulture)} {UnitsHelper.GetShortString(group.Units)}",
}.Where(s => !string.IsNullOrWhiteSpace(s)));
// The nest is not shown anywhere yet, so Auto Nest runs off the UI thread on it alone.
private static Task<SetReleaseRenest> RenestAsync(Nest nest, CancellationToken token)
{
var engineName = EngineSelection.EngineName;
return Task.Run(() =>
{
var renested = NestRenest.Run(nest, engineName, AutoNestForm.DefaultMinRemnantSize, token: token);
return new SetReleaseRenest(renested.Nested, renested.Note);
}, token);
}
private static LibraryPackage ReadPackage(byte[] bytes)
{
var contents = DrawingPackage.Read(bytes);
+18 -6
View File
@@ -404,10 +404,21 @@ survives changing the sets, kits or the archived toggle.
**Nest** is **New Draft nest** or **Merge into** one of the Draft nests of the
group's material, grade and thickness, listed with their sheet count and when
they were saved. A merge discards that draft's layout and nests the whole demand
again (the status column says so); **Undo Release** restores it. A draft open in
a tab of this desktop is refused, since that tab could hold edits the merge would
leave behind: close it first.
they were saved. A merge adds the demand to that draft's drawings and discards
every sheet (placements, cutoffs, cut plans), keeping drawing-level setup such as
cut pins (the status column says so); **Undo Release** restores it. A draft open
in a tab of this desktop is refused, since that tab could hold edits the merge
would leave behind: close it first.
**Nest after release** (ticked by default) runs Auto Nest on every released
group, new or merged, before anything is saved (`OpenNest.Engine.Jobs.NestRenest`):
every drawing's required quantity, the selected engine, sheets from the nest's
plate defaults and plate-size options, as the Auto Nest dialog would offer them.
A layout is kept only when it is valid; otherwise the nest is saved without
sheets and its status says "Auto Nest could not keep a layout: open it and run
Auto Nest". The status column lists each group's sheets and utilization, and any
copies left unplaced, which stay as remaining quantity. Unticked, new nests are
saved without sheets and merged drafts are left cleared for Auto Nest.
**Release** confirms, then runs `OpenNest.Data.SetReleaseSession`: every ticked
group's new nest is made as File > New would (the server's next nest name, the
@@ -418,8 +429,9 @@ and the **Draft** status. A merged draft is saved only while nobody else has
saved it since it was listed. Each created or merged nest opens in its own tab,
bound to its server record, and the release (with the release notes) is
recorded. The grid then shows every group's outcome: Created or Merged into with
the nest name, Refused or Failed with the reason, or Not started. Cancel stops
between saves, never during one. When every nest was saved but recording failed,
the nest name, Refused or Failed with the reason, or Not started. Cancel during
preparing or nesting saves nothing; once saving starts it stops between saves,
never during one. When every nest was saved but recording failed,
the button becomes **Record Release** and posts the same release again. A dialog
runs one release.