refactor(ui): route every Auto Nest engine through the validated pipeline
This commit is contained in:
@@ -0,0 +1,90 @@
|
|||||||
|
using OpenNest.Engine.Jobs;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
|
namespace OpenNest.Engine.Tests.Jobs;
|
||||||
|
|
||||||
|
public class NestPipelineCommitTests
|
||||||
|
{
|
||||||
|
private sealed class StubEngine(bool overlap) : INestingEngine
|
||||||
|
{
|
||||||
|
public NestJobResult Solve(NestJob job, IProgress<NestJobProgress>? progress = null,
|
||||||
|
CancellationToken token = default) => new(
|
||||||
|
NestJobStatus.Complete, NestJobStopReason.Completed,
|
||||||
|
new[] { new NestJobPlateResult(0, job.Plates[0], new[]
|
||||||
|
{
|
||||||
|
new NestJobPlacement(job.Parts[0].Id, 0, 2, 2, 0),
|
||||||
|
new NestJobPlacement(job.Parts[0].Id, 1, overlap ? 2 : 20, 2, 0),
|
||||||
|
}) },
|
||||||
|
new[] { new PartFulfillment(job.Parts[0].Id, 2, 2, 0) },
|
||||||
|
new[] { new StockUsage(job.Plates[0].Id, 1, null) });
|
||||||
|
}
|
||||||
|
|
||||||
|
private static NestPipelineResult Result(Drawing drawing, bool overlap = false) =>
|
||||||
|
NestPipeline.Run(new StubEngine(overlap), "stub", new NestPipelineRequest("stub",
|
||||||
|
new[] { new NestItem { Drawing = drawing, Quantity = 2 } },
|
||||||
|
new[] { new NestPlateStock("sheet", new Size(48, 96), null, 0.25, new Spacing(1, 1), 1) }));
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AppliesValidatedSettingsToEmptyPlateAndNeverFillsOccupiedPlate()
|
||||||
|
{
|
||||||
|
var drawing = new Drawing("part", TestDrawingFactory.Rectangle());
|
||||||
|
var nest = new Nest();
|
||||||
|
nest.Drawings.Add(drawing);
|
||||||
|
var occupied = nest.CreatePlate();
|
||||||
|
occupied.Parts.Add(new Part(drawing));
|
||||||
|
var empty = nest.CreatePlate();
|
||||||
|
empty.Quantity = 7;
|
||||||
|
empty.Quadrant = 3;
|
||||||
|
empty.Size = new Size(4, 8);
|
||||||
|
empty.EdgeSpacing = new Spacing(0, 0);
|
||||||
|
using var manager = new PlateManager(nest);
|
||||||
|
var result = Result(drawing);
|
||||||
|
Assert.True(result.IsValid, string.Join("; ", result.Violations));
|
||||||
|
|
||||||
|
var applied = NestPipelineCommit.ApplyToEmptyPlates(result, manager);
|
||||||
|
|
||||||
|
Assert.Same(empty, Assert.Single(applied));
|
||||||
|
Assert.Single(occupied.Parts);
|
||||||
|
Assert.Equal(2, empty.Parts.Count);
|
||||||
|
Assert.Equal(1, empty.Quantity);
|
||||||
|
Assert.Equal(1, empty.Quadrant);
|
||||||
|
Assert.Equal(result.Job.Plates[0].Size, empty.Size);
|
||||||
|
Assert.Equal(result.Job.Plates[0].EdgeSpacing, empty.EdgeSpacing);
|
||||||
|
Assert.Equal(0.25, empty.PartSpacing);
|
||||||
|
Assert.All(empty.Parts, p => Assert.Same(drawing, p.BaseDrawing));
|
||||||
|
Assert.Equal(3, drawing.Quantity.Nested);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidResultRequiresExplicitConsentAndDiscardDoesNotMutateNest()
|
||||||
|
{
|
||||||
|
var drawing = new Drawing("part", TestDrawingFactory.Rectangle());
|
||||||
|
var nest = new Nest();
|
||||||
|
nest.Drawings.Add(drawing);
|
||||||
|
var empty = nest.CreatePlate();
|
||||||
|
using var manager = new PlateManager(nest);
|
||||||
|
var result = Result(drawing, overlap: true);
|
||||||
|
Assert.False(result.IsValid);
|
||||||
|
|
||||||
|
Assert.Throws<InvalidOperationException>(() => NestPipelineCommit.ApplyToEmptyPlates(result, manager));
|
||||||
|
Assert.Same(empty, Assert.Single(nest.Plates));
|
||||||
|
Assert.Empty(empty.Parts);
|
||||||
|
Assert.Equal(0, drawing.Quantity.Nested);
|
||||||
|
|
||||||
|
var applied = NestPipelineCommit.ApplyToEmptyPlates(result, manager, allowInvalid: true);
|
||||||
|
Assert.Equal(2, Assert.Single(applied).Parts.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CancelledCommitDoesNotCreateAnyPlate()
|
||||||
|
{
|
||||||
|
var drawing = new Drawing("part", TestDrawingFactory.Rectangle());
|
||||||
|
var nest = new Nest();
|
||||||
|
using var manager = new PlateManager(nest);
|
||||||
|
using var cts = new CancellationTokenSource();
|
||||||
|
cts.Cancel();
|
||||||
|
Assert.ThrowsAny<OperationCanceledException>(() =>
|
||||||
|
NestPipelineCommit.ApplyToEmptyPlates(Result(drawing), manager, token: cts.Token));
|
||||||
|
Assert.Empty(nest.Plates);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace OpenNest.Engine.Jobs;
|
||||||
|
|
||||||
|
/// <summary>Applies an explicitly accepted whole-job proposal to empty physical sheets only.
|
||||||
|
/// Caller must keep the nest and its drawings stable from request construction through commit.</summary>
|
||||||
|
public static class NestPipelineCommit
|
||||||
|
{
|
||||||
|
public static IReadOnlyList<Plate> ApplyToEmptyPlates(
|
||||||
|
NestPipelineResult result,
|
||||||
|
PlateManager manager,
|
||||||
|
bool allowInvalid = false,
|
||||||
|
CancellationToken token = default
|
||||||
|
)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(result);
|
||||||
|
ArgumentNullException.ThrowIfNull(manager);
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
if (!result.CanKeep || (!result.IsValid && !allowInvalid))
|
||||||
|
throw new InvalidOperationException("The nesting result cannot be committed without a keepable layout and explicit consent to its violations.");
|
||||||
|
|
||||||
|
var applied = new List<Plate>();
|
||||||
|
// Commit is synchronous on the caller's owning thread. Cancellation is checked
|
||||||
|
// before mutation, not partway through attachment (which would leave half a job).
|
||||||
|
manager.BeginBatch();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var proposed in result.Plates)
|
||||||
|
{
|
||||||
|
if (proposed.Parts.Count == 0)
|
||||||
|
continue;
|
||||||
|
var plate = manager.GetOrCreateEmpty();
|
||||||
|
plate.Size = proposed.Stock.Size;
|
||||||
|
plate.PartSpacing = proposed.Stock.PartSpacing;
|
||||||
|
plate.EdgeSpacing = proposed.Stock.EdgeSpacing;
|
||||||
|
plate.Quadrant = proposed.Stock.Quadrant;
|
||||||
|
plate.Quantity = 1;
|
||||||
|
plate.Parts.AddRange(proposed.Parts);
|
||||||
|
applied.Add(plate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
manager.EndBatch();
|
||||||
|
}
|
||||||
|
return applied.AsReadOnly();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
using System.Runtime.ExceptionServices;
|
||||||
|
using System.Windows.Forms;
|
||||||
|
using OpenNest.Forms;
|
||||||
|
|
||||||
|
namespace OpenNest.WinForms.Tests.Forms;
|
||||||
|
|
||||||
|
public class NestValidationFormTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData(true)]
|
||||||
|
[InlineData(false)]
|
||||||
|
public void DiscardIsDefaultAndMalformedOutputCannotBeKept(bool canKeep)
|
||||||
|
{
|
||||||
|
RunSta(() =>
|
||||||
|
{
|
||||||
|
using var form = new NestValidationForm(new[] { "part A and part B violate spacing" }, canKeep);
|
||||||
|
var buttons = form.Controls.OfType<FlowLayoutPanel>().Single().Controls.OfType<Button>().ToArray();
|
||||||
|
var discard = Assert.Single(buttons, b => b.Text == "Discard");
|
||||||
|
var keep = Assert.Single(buttons, b => b.Text == "Keep anyway");
|
||||||
|
Assert.Same(discard, form.AcceptButton);
|
||||||
|
Assert.Same(discard, form.CancelButton);
|
||||||
|
Assert.Equal(canKeep, keep.Enabled);
|
||||||
|
Assert.Contains("violate spacing", form.Controls.OfType<TextBox>().Single().Text);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TestProgress(CancellationTokenSource cts) : NestProgressForm(cts)
|
||||||
|
{
|
||||||
|
public FormClosingEventArgs RequestClose()
|
||||||
|
{
|
||||||
|
var args = new FormClosingEventArgs(CloseReason.UserClosing, false);
|
||||||
|
OnFormClosing(args);
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ClosingWholeJobProgressWaitsForWorkerAndRequestsCancellation()
|
||||||
|
{
|
||||||
|
RunSta(() =>
|
||||||
|
{
|
||||||
|
using var cts = new CancellationTokenSource();
|
||||||
|
using var form = new TestProgress(cts) { HoldOpenUntilCompleted = true };
|
||||||
|
Assert.True(form.Controls.Find("stopButton", true).Single().Enabled);
|
||||||
|
Assert.True(form.RequestClose().Cancel);
|
||||||
|
Assert.True(cts.IsCancellationRequested);
|
||||||
|
form.ShowCompleted();
|
||||||
|
Assert.False(form.RequestClose().Cancel);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CompletionClosesWithoutCancellingSuccessfulJob()
|
||||||
|
{
|
||||||
|
RunSta(() =>
|
||||||
|
{
|
||||||
|
using var cts = new CancellationTokenSource();
|
||||||
|
using var form = new TestProgress(cts) { HoldOpenUntilCompleted = true };
|
||||||
|
form.ShowCompleted();
|
||||||
|
Assert.False(form.RequestClose().Cancel);
|
||||||
|
Assert.False(cts.IsCancellationRequested);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RunSta(Action action)
|
||||||
|
{
|
||||||
|
Exception? error = null;
|
||||||
|
var thread = new Thread(() => { try { action(); } catch (Exception ex) { error = ex; } });
|
||||||
|
thread.SetApartmentState(ApartmentState.STA);
|
||||||
|
thread.Start();
|
||||||
|
Assert.True(thread.Join(TimeSpan.FromSeconds(15)));
|
||||||
|
if (error != null)
|
||||||
|
ExceptionDispatchInfo.Capture(error).Throw();
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+8
-73
@@ -24,11 +24,6 @@ namespace OpenNest.Forms
|
|||||||
this.summaryLabel = new System.Windows.Forms.Label();
|
this.summaryLabel = new System.Windows.Forms.Label();
|
||||||
this.engineLabel = new System.Windows.Forms.Label();
|
this.engineLabel = new System.Windows.Forms.Label();
|
||||||
this.engineComboBox = new System.Windows.Forms.ComboBox();
|
this.engineComboBox = new System.Windows.Forms.ComboBox();
|
||||||
this.createNewPlatesAsNeededBox = new System.Windows.Forms.CheckBox();
|
|
||||||
this.partFirstGroup = new System.Windows.Forms.GroupBox();
|
|
||||||
this.partFirstCheckBox = new System.Windows.Forms.CheckBox();
|
|
||||||
this.sortOrderLabel = new System.Windows.Forms.Label();
|
|
||||||
this.sortOrderComboBox = new System.Windows.Forms.ComboBox();
|
|
||||||
this.minRemnantLabel = new System.Windows.Forms.Label();
|
this.minRemnantLabel = new System.Windows.Forms.Label();
|
||||||
this.minRemnantBox = new System.Windows.Forms.TextBox();
|
this.minRemnantBox = new System.Windows.Forms.TextBox();
|
||||||
this.plateOptimizerGroup = new System.Windows.Forms.GroupBox();
|
this.plateOptimizerGroup = new System.Windows.Forms.GroupBox();
|
||||||
@@ -45,7 +40,6 @@ namespace OpenNest.Forms
|
|||||||
this.platesTab.SuspendLayout();
|
this.platesTab.SuspendLayout();
|
||||||
((System.ComponentModel.ISupportInitialize)(this.partsGrid)).BeginInit();
|
((System.ComponentModel.ISupportInitialize)(this.partsGrid)).BeginInit();
|
||||||
((System.ComponentModel.ISupportInitialize)(this.plateGrid)).BeginInit();
|
((System.ComponentModel.ISupportInitialize)(this.plateGrid)).BeginInit();
|
||||||
this.partFirstGroup.SuspendLayout();
|
|
||||||
this.plateOptimizerGroup.SuspendLayout();
|
this.plateOptimizerGroup.SuspendLayout();
|
||||||
this.buttonPanel.SuspendLayout();
|
this.buttonPanel.SuspendLayout();
|
||||||
this.SuspendLayout();
|
this.SuspendLayout();
|
||||||
@@ -77,8 +71,6 @@ namespace OpenNest.Forms
|
|||||||
//
|
//
|
||||||
this.platesTab.Controls.Add(this.engineLabel);
|
this.platesTab.Controls.Add(this.engineLabel);
|
||||||
this.platesTab.Controls.Add(this.engineComboBox);
|
this.platesTab.Controls.Add(this.engineComboBox);
|
||||||
this.platesTab.Controls.Add(this.createNewPlatesAsNeededBox);
|
|
||||||
this.platesTab.Controls.Add(this.partFirstGroup);
|
|
||||||
this.platesTab.Controls.Add(this.plateOptimizerGroup);
|
this.platesTab.Controls.Add(this.plateOptimizerGroup);
|
||||||
this.platesTab.Location = new System.Drawing.Point(4, 25);
|
this.platesTab.Location = new System.Drawing.Point(4, 25);
|
||||||
this.platesTab.Name = "platesTab";
|
this.platesTab.Name = "platesTab";
|
||||||
@@ -129,70 +121,18 @@ namespace OpenNest.Forms
|
|||||||
this.engineComboBox.Size = new System.Drawing.Size(200, 24);
|
this.engineComboBox.Size = new System.Drawing.Size(200, 24);
|
||||||
this.engineComboBox.TabIndex = 1;
|
this.engineComboBox.TabIndex = 1;
|
||||||
//
|
//
|
||||||
// createNewPlatesAsNeededBox
|
|
||||||
//
|
|
||||||
this.createNewPlatesAsNeededBox.AutoSize = true;
|
|
||||||
this.createNewPlatesAsNeededBox.Location = new System.Drawing.Point(10, 44);
|
|
||||||
this.createNewPlatesAsNeededBox.Name = "createNewPlatesAsNeededBox";
|
|
||||||
this.createNewPlatesAsNeededBox.Size = new System.Drawing.Size(202, 20);
|
|
||||||
this.createNewPlatesAsNeededBox.TabIndex = 2;
|
|
||||||
this.createNewPlatesAsNeededBox.Text = "Create new plates as needed";
|
|
||||||
this.createNewPlatesAsNeededBox.UseVisualStyleBackColor = true;
|
|
||||||
//
|
|
||||||
// partFirstGroup
|
|
||||||
//
|
|
||||||
this.partFirstGroup.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right)));
|
|
||||||
this.partFirstGroup.Controls.Add(this.partFirstCheckBox);
|
|
||||||
this.partFirstGroup.Controls.Add(this.sortOrderLabel);
|
|
||||||
this.partFirstGroup.Controls.Add(this.sortOrderComboBox);
|
|
||||||
this.partFirstGroup.Controls.Add(this.minRemnantLabel);
|
|
||||||
this.partFirstGroup.Controls.Add(this.minRemnantBox);
|
|
||||||
this.partFirstGroup.Location = new System.Drawing.Point(10, 72);
|
|
||||||
this.partFirstGroup.Name = "partFirstGroup";
|
|
||||||
this.partFirstGroup.Size = new System.Drawing.Size(528, 80);
|
|
||||||
this.partFirstGroup.TabIndex = 3;
|
|
||||||
this.partFirstGroup.TabStop = false;
|
|
||||||
this.partFirstGroup.Text = " Part-First Mode";
|
|
||||||
//
|
|
||||||
// partFirstCheckBox
|
|
||||||
//
|
|
||||||
this.partFirstCheckBox.AutoSize = true;
|
|
||||||
this.partFirstCheckBox.Location = new System.Drawing.Point(10, 0);
|
|
||||||
this.partFirstCheckBox.Name = "partFirstCheckBox";
|
|
||||||
this.partFirstCheckBox.Size = new System.Drawing.Size(15, 14);
|
|
||||||
this.partFirstCheckBox.TabIndex = 0;
|
|
||||||
this.partFirstCheckBox.UseVisualStyleBackColor = true;
|
|
||||||
this.partFirstCheckBox.CheckedChanged += new System.EventHandler(this.partFirstCheckBox_CheckedChanged);
|
|
||||||
//
|
|
||||||
// sortOrderLabel
|
|
||||||
//
|
|
||||||
this.sortOrderLabel.AutoSize = true;
|
|
||||||
this.sortOrderLabel.Location = new System.Drawing.Point(10, 26);
|
|
||||||
this.sortOrderLabel.Name = "sortOrderLabel";
|
|
||||||
this.sortOrderLabel.Size = new System.Drawing.Size(75, 16);
|
|
||||||
this.sortOrderLabel.TabIndex = 1;
|
|
||||||
this.sortOrderLabel.Text = "Sort Order:";
|
|
||||||
//
|
|
||||||
// sortOrderComboBox
|
|
||||||
//
|
|
||||||
this.sortOrderComboBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
|
||||||
this.sortOrderComboBox.Location = new System.Drawing.Point(100, 23);
|
|
||||||
this.sortOrderComboBox.Name = "sortOrderComboBox";
|
|
||||||
this.sortOrderComboBox.Size = new System.Drawing.Size(180, 24);
|
|
||||||
this.sortOrderComboBox.TabIndex = 2;
|
|
||||||
//
|
|
||||||
// minRemnantLabel
|
// minRemnantLabel
|
||||||
//
|
//
|
||||||
this.minRemnantLabel.AutoSize = true;
|
this.minRemnantLabel.AutoSize = true;
|
||||||
this.minRemnantLabel.Location = new System.Drawing.Point(10, 54);
|
this.minRemnantLabel.Location = new System.Drawing.Point(10, 193);
|
||||||
this.minRemnantLabel.Name = "minRemnantLabel";
|
this.minRemnantLabel.Name = "minRemnantLabel";
|
||||||
this.minRemnantLabel.Size = new System.Drawing.Size(117, 16);
|
this.minRemnantLabel.Size = new System.Drawing.Size(117, 16);
|
||||||
this.minRemnantLabel.TabIndex = 3;
|
this.minRemnantLabel.TabIndex = 3;
|
||||||
this.minRemnantLabel.Text = "Min Remnant Size:";
|
this.minRemnantLabel.Text = "Min Salvage Size:";
|
||||||
//
|
//
|
||||||
// minRemnantBox
|
// minRemnantBox
|
||||||
//
|
//
|
||||||
this.minRemnantBox.Location = new System.Drawing.Point(133, 51);
|
this.minRemnantBox.Location = new System.Drawing.Point(155, 190);
|
||||||
this.minRemnantBox.Name = "minRemnantBox";
|
this.minRemnantBox.Name = "minRemnantBox";
|
||||||
this.minRemnantBox.Size = new System.Drawing.Size(60, 22);
|
this.minRemnantBox.Size = new System.Drawing.Size(60, 22);
|
||||||
this.minRemnantBox.TabIndex = 4;
|
this.minRemnantBox.TabIndex = 4;
|
||||||
@@ -201,17 +141,19 @@ namespace OpenNest.Forms
|
|||||||
// plateOptimizerGroup
|
// plateOptimizerGroup
|
||||||
//
|
//
|
||||||
this.plateOptimizerGroup.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right)));
|
this.plateOptimizerGroup.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right)));
|
||||||
|
this.plateOptimizerGroup.Controls.Add(this.minRemnantLabel);
|
||||||
|
this.plateOptimizerGroup.Controls.Add(this.minRemnantBox);
|
||||||
this.plateOptimizerGroup.Controls.Add(this.optimizePlateSizeBox);
|
this.plateOptimizerGroup.Controls.Add(this.optimizePlateSizeBox);
|
||||||
this.plateOptimizerGroup.Controls.Add(this.plateGrid);
|
this.plateOptimizerGroup.Controls.Add(this.plateGrid);
|
||||||
this.plateOptimizerGroup.Controls.Add(this.salvageRateLabel);
|
this.plateOptimizerGroup.Controls.Add(this.salvageRateLabel);
|
||||||
this.plateOptimizerGroup.Controls.Add(this.salvageRateBox);
|
this.plateOptimizerGroup.Controls.Add(this.salvageRateBox);
|
||||||
this.plateOptimizerGroup.Controls.Add(this.salvageRatePercentLabel);
|
this.plateOptimizerGroup.Controls.Add(this.salvageRatePercentLabel);
|
||||||
this.plateOptimizerGroup.Location = new System.Drawing.Point(10, 158);
|
this.plateOptimizerGroup.Location = new System.Drawing.Point(10, 52);
|
||||||
this.plateOptimizerGroup.Name = "plateOptimizerGroup";
|
this.plateOptimizerGroup.Name = "plateOptimizerGroup";
|
||||||
this.plateOptimizerGroup.Size = new System.Drawing.Size(528, 188);
|
this.plateOptimizerGroup.Size = new System.Drawing.Size(528, 224);
|
||||||
this.plateOptimizerGroup.TabIndex = 4;
|
this.plateOptimizerGroup.TabIndex = 4;
|
||||||
this.plateOptimizerGroup.TabStop = false;
|
this.plateOptimizerGroup.TabStop = false;
|
||||||
this.plateOptimizerGroup.Text = " Plate Optimizer";
|
this.plateOptimizerGroup.Text = " Stock Options";
|
||||||
//
|
//
|
||||||
// optimizePlateSizeBox
|
// optimizePlateSizeBox
|
||||||
//
|
//
|
||||||
@@ -318,8 +260,6 @@ namespace OpenNest.Forms
|
|||||||
this.platesTab.PerformLayout();
|
this.platesTab.PerformLayout();
|
||||||
((System.ComponentModel.ISupportInitialize)(this.partsGrid)).EndInit();
|
((System.ComponentModel.ISupportInitialize)(this.partsGrid)).EndInit();
|
||||||
((System.ComponentModel.ISupportInitialize)(this.plateGrid)).EndInit();
|
((System.ComponentModel.ISupportInitialize)(this.plateGrid)).EndInit();
|
||||||
this.partFirstGroup.ResumeLayout(false);
|
|
||||||
this.partFirstGroup.PerformLayout();
|
|
||||||
this.plateOptimizerGroup.ResumeLayout(false);
|
this.plateOptimizerGroup.ResumeLayout(false);
|
||||||
this.plateOptimizerGroup.PerformLayout();
|
this.plateOptimizerGroup.PerformLayout();
|
||||||
this.buttonPanel.ResumeLayout(false);
|
this.buttonPanel.ResumeLayout(false);
|
||||||
@@ -335,11 +275,6 @@ namespace OpenNest.Forms
|
|||||||
private System.Windows.Forms.Label summaryLabel;
|
private System.Windows.Forms.Label summaryLabel;
|
||||||
private System.Windows.Forms.Label engineLabel;
|
private System.Windows.Forms.Label engineLabel;
|
||||||
private System.Windows.Forms.ComboBox engineComboBox;
|
private System.Windows.Forms.ComboBox engineComboBox;
|
||||||
private System.Windows.Forms.CheckBox createNewPlatesAsNeededBox;
|
|
||||||
private System.Windows.Forms.GroupBox partFirstGroup;
|
|
||||||
private System.Windows.Forms.CheckBox partFirstCheckBox;
|
|
||||||
private System.Windows.Forms.Label sortOrderLabel;
|
|
||||||
private System.Windows.Forms.ComboBox sortOrderComboBox;
|
|
||||||
private System.Windows.Forms.Label minRemnantLabel;
|
private System.Windows.Forms.Label minRemnantLabel;
|
||||||
private System.Windows.Forms.TextBox minRemnantBox;
|
private System.Windows.Forms.TextBox minRemnantBox;
|
||||||
private System.Windows.Forms.GroupBox plateOptimizerGroup;
|
private System.Windows.Forms.GroupBox plateOptimizerGroup;
|
||||||
|
|||||||
@@ -22,11 +22,6 @@ namespace OpenNest.Forms
|
|||||||
LoadDefaultPlateOptions();
|
LoadDefaultPlateOptions();
|
||||||
SetPlateOptimizerVisible(false);
|
SetPlateOptimizerVisible(false);
|
||||||
|
|
||||||
sortOrderComboBox.Items.Add("Bounding Box Area");
|
|
||||||
sortOrderComboBox.Items.Add("Size");
|
|
||||||
sortOrderComboBox.SelectedIndex = 0;
|
|
||||||
SetPartFirstVisible(false);
|
|
||||||
|
|
||||||
partsGrid.DataError += PartsGrid_DataError;
|
partsGrid.DataError += PartsGrid_DataError;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,12 +31,6 @@ namespace OpenNest.Forms
|
|||||||
set { engineComboBox.SelectedItem = value; }
|
set { engineComboBox.SelectedItem = value; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool AllowPlateCreation
|
|
||||||
{
|
|
||||||
get { return createNewPlatesAsNeededBox.Checked; }
|
|
||||||
set { createNewPlatesAsNeededBox.Checked = value; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool OptimizePlateSize
|
public bool OptimizePlateSize
|
||||||
{
|
{
|
||||||
get { return optimizePlateSizeBox.Checked; }
|
get { return optimizePlateSizeBox.Checked; }
|
||||||
@@ -59,22 +48,6 @@ namespace OpenNest.Forms
|
|||||||
set { salvageRateBox.Text = (value * 100).ToString("F0"); }
|
set { salvageRateBox.Text = (value * 100).ToString("F0"); }
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool PartFirstMode
|
|
||||||
{
|
|
||||||
get { return partFirstCheckBox.Checked; }
|
|
||||||
set { partFirstCheckBox.Checked = value; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public PartSortOrder SortOrder
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (sortOrderComboBox.SelectedItem is string s && s == "Size")
|
|
||||||
return PartSortOrder.Size;
|
|
||||||
return PartSortOrder.BoundingBoxArea;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public double MinRemnantSize
|
public double MinRemnantSize
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
@@ -292,17 +265,6 @@ namespace OpenNest.Forms
|
|||||||
salvageRateLabel.Visible = visible;
|
salvageRateLabel.Visible = visible;
|
||||||
salvageRateBox.Visible = visible;
|
salvageRateBox.Visible = visible;
|
||||||
salvageRatePercentLabel.Visible = visible;
|
salvageRatePercentLabel.Visible = visible;
|
||||||
}
|
|
||||||
|
|
||||||
private void partFirstCheckBox_CheckedChanged(object sender, EventArgs e)
|
|
||||||
{
|
|
||||||
SetPartFirstVisible(partFirstCheckBox.Checked);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SetPartFirstVisible(bool visible)
|
|
||||||
{
|
|
||||||
sortOrderLabel.Visible = visible;
|
|
||||||
sortOrderComboBox.Visible = visible;
|
|
||||||
minRemnantLabel.Visible = visible;
|
minRemnantLabel.Visible = visible;
|
||||||
minRemnantBox.Visible = visible;
|
minRemnantBox.Visible = visible;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,91 +9,11 @@ using OpenNest.Geometry;
|
|||||||
namespace OpenNest.Forms
|
namespace OpenNest.Forms
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Desktop adapter for whole-job engines (StockLadder and Engines/ plug-ins): builds the
|
/// Desktop progress adapter for every whole-job engine. Building, validation and binding
|
||||||
/// NestJob from auto-nest items, translates NestJobProgress for NestProgressForm, and maps
|
/// belong to the shared NestPipeline.
|
||||||
/// result poses back onto the nest's own drawings.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static class JobEngineNest
|
internal static class JobEngineNest
|
||||||
{
|
{
|
||||||
public static NestJob BuildJob(
|
|
||||||
IReadOnlyList<NestItem> items,
|
|
||||||
Plate template,
|
|
||||||
List<PlateOption> plateOptions,
|
|
||||||
double salvageRate,
|
|
||||||
double minRemnantSize,
|
|
||||||
int maxPlates,
|
|
||||||
out Dictionary<string, Drawing> drawingsByPartId
|
|
||||||
)
|
|
||||||
{
|
|
||||||
var parts = new List<NestJobPart>();
|
|
||||||
drawingsByPartId = new Dictionary<string, Drawing>(StringComparer.Ordinal);
|
|
||||||
|
|
||||||
for (var i = 0; i < items.Count; i++)
|
|
||||||
{
|
|
||||||
if (items[i].Quantity <= 0)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var partId = $"part-{i}";
|
|
||||||
parts.Add(DrawingJobMapper.FromItem(partId, items[i]));
|
|
||||||
drawingsByPartId[partId] = items[i].Drawing;
|
|
||||||
}
|
|
||||||
|
|
||||||
var stock = new List<NestPlateStock>();
|
|
||||||
|
|
||||||
if (plateOptions != null && plateOptions.Count > 0)
|
|
||||||
{
|
|
||||||
for (var i = 0; i < plateOptions.Count; i++)
|
|
||||||
{
|
|
||||||
var option = plateOptions[i];
|
|
||||||
stock.Add(
|
|
||||||
new NestPlateStock(
|
|
||||||
$"option-{i}",
|
|
||||||
new Size(option.Width, option.Length),
|
|
||||||
quantity: null,
|
|
||||||
template.PartSpacing,
|
|
||||||
template.EdgeSpacing,
|
|
||||||
template.Quadrant
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
stock.Add(DrawingJobMapper.FromPlate("plate", template, quantity: null));
|
|
||||||
}
|
|
||||||
|
|
||||||
var options = new NestJobOptions(
|
|
||||||
maxPlates: maxPlates,
|
|
||||||
salvageRate: plateOptions != null && plateOptions.Count > 0 ? salvageRate : 0,
|
|
||||||
minimumSalvageDimension: minRemnantSize
|
|
||||||
);
|
|
||||||
|
|
||||||
return new NestJob(parts, stock, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Parts for one result sheet, bound to the caller's drawings.</summary>
|
|
||||||
public static List<Part> CreateParts(
|
|
||||||
NestJobPlateResult sheet,
|
|
||||||
IReadOnlyDictionary<string, Drawing> drawingsByPartId
|
|
||||||
)
|
|
||||||
{
|
|
||||||
var parts = new List<Part>(sheet.Placements.Count);
|
|
||||||
|
|
||||||
foreach (var pose in sheet.Placements)
|
|
||||||
{
|
|
||||||
if (!drawingsByPartId.TryGetValue(pose.PartId, out var drawing))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var part = new Part(drawing);
|
|
||||||
part.Rotate(pose.Rotation);
|
|
||||||
part.Location = new Vector(pose.X, pose.Y);
|
|
||||||
part.UpdateBounds();
|
|
||||||
parts.Add(part);
|
|
||||||
}
|
|
||||||
|
|
||||||
return parts;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Forwards job progress to a NestProgress sink. An engine's optional LegacyProgress detail
|
/// Forwards job progress to a NestProgress sink. An engine's optional LegacyProgress detail
|
||||||
/// (live preview parts) passes through; otherwise the stage and committed counts become a
|
/// (live preview parts) passes through; otherwise the stage and committed counts become a
|
||||||
|
|||||||
+98
-302
@@ -1146,336 +1146,132 @@ namespace OpenNest.Forms
|
|||||||
|
|
||||||
private async void RunAutoNest_Click(object sender, EventArgs e)
|
private async void RunAutoNest_Click(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
var form = new AutoNestForm(activeForm.Nest);
|
var target = activeForm;
|
||||||
form.AllowPlateCreation = true;
|
if (target == null || target.IsDisposed || target.PlateView.Plate == null)
|
||||||
|
|
||||||
if (activeForm.Nest.PlateOptions.Count > 0)
|
|
||||||
form.LoadPlateOptions(activeForm.Nest.PlateOptions, activeForm.Nest.SalvageRate);
|
|
||||||
|
|
||||||
if (form.ShowDialog() != System.Windows.Forms.DialogResult.OK)
|
|
||||||
return;
|
return;
|
||||||
|
var views = MdiChildren.OfType<EditNestForm>()
|
||||||
if (form.EngineName != null)
|
.Where(f => ReferenceEquals(f.Nest, target.Nest)).Select(f => f.PlateView).ToArray();
|
||||||
|
if (nestingInProgress || Application.OpenForms.OfType<NestProgressForm>().Any()
|
||||||
|
|| views.Any(v => v.IsFillInProgress || v.Actions.CurrentAction?.IsBusy() == true))
|
||||||
{
|
{
|
||||||
EngineSelection.EngineName = form.EngineName;
|
MessageBox.Show(this, "Finish or cancel the current plate operation before Auto Nest.",
|
||||||
engineComboBox.SelectedItem = form.EngineName;
|
"Auto Nest", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
foreach (var view in views)
|
||||||
|
view.SetAction(typeof(ActionSelect));
|
||||||
|
|
||||||
|
using var form = new AutoNestForm(target.Nest);
|
||||||
|
if (target.Nest.PlateOptions.Count > 0)
|
||||||
|
form.LoadPlateOptions(target.Nest.PlateOptions, target.Nest.SalvageRate);
|
||||||
|
if (form.ShowDialog(this) != DialogResult.OK)
|
||||||
|
return;
|
||||||
|
|
||||||
var items = form.GetNestItems();
|
var items = form.GetNestItems();
|
||||||
|
|
||||||
if (!items.Any(it => it.Quantity > 0))
|
if (!items.Any(it => it.Quantity > 0))
|
||||||
return;
|
return;
|
||||||
|
var engineName = form.EngineName ?? EngineSelection.EngineName;
|
||||||
var optimizePlateSize = form.OptimizePlateSize;
|
EngineSelection.EngineName = engineName;
|
||||||
var plateOptions = optimizePlateSize ? form.GetPlateOptions() : null;
|
engineComboBox.SelectedItem = engineName;
|
||||||
var salvageRate = form.SalvageRate;
|
var plateOptions = form.OptimizePlateSize ? form.GetPlateOptions() : null;
|
||||||
var partFirstMode = form.PartFirstMode;
|
using var cts = new CancellationTokenSource();
|
||||||
var sortOrder = form.SortOrder;
|
nestingCts = cts;
|
||||||
var minRemnantSize = form.MinRemnantSize;
|
using var progressForm = new NestProgressForm(cts, showPlateRow: true)
|
||||||
var allowPlateCreation = form.AllowPlateCreation;
|
|
||||||
|
|
||||||
if (optimizePlateSize)
|
|
||||||
{
|
{
|
||||||
activeForm.Nest.PlateOptions = plateOptions;
|
AllowAccept = false,
|
||||||
activeForm.Nest.SalvageRate = salvageRate;
|
HoldOpenUntilCompleted = true,
|
||||||
}
|
};
|
||||||
|
var receivingProgress = true;
|
||||||
nestingCts = new CancellationTokenSource();
|
|
||||||
var progressForm = new NestProgressForm(nestingCts, showPlateRow: true);
|
|
||||||
|
|
||||||
var jobEngineName = EngineSelection.IsFillStrategy(EngineSelection.EngineName)
|
|
||||||
? null
|
|
||||||
: EngineSelection.EngineName;
|
|
||||||
progressForm.AllowAccept = jobEngineName == null;
|
|
||||||
|
|
||||||
var progress = new Progress<NestProgress>(p =>
|
var progress = new Progress<NestProgress>(p =>
|
||||||
{
|
{
|
||||||
|
if (!receivingProgress || target.IsDisposed || cts.IsCancellationRequested)
|
||||||
|
return;
|
||||||
progressForm.UpdateProgress(p);
|
progressForm.UpdateProgress(p);
|
||||||
|
|
||||||
if (p.IsOverallBest)
|
if (p.IsOverallBest)
|
||||||
activeForm.PlateView.SetActiveParts(p.BestParts);
|
target.PlateView.SetActiveParts(p.BestParts);
|
||||||
activeForm.PlateView.ActiveWorkArea = p.ActiveWorkArea;
|
target.PlateView.ActiveWorkArea = p.ActiveWorkArea;
|
||||||
});
|
});
|
||||||
|
var jobProgress = JobEngineNest.CreateProgress(engineName, progress);
|
||||||
progressForm.Show(this);
|
|
||||||
SetNestingLockout(true);
|
SetNestingLockout(true);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (jobEngineName != null)
|
var request = new NestPipelineRequest(engineName, items,
|
||||||
await RunJobEngineAsync(
|
NestStockBuilder.FromTemplate(target.PlateView.Plate, plateOptions),
|
||||||
jobEngineName,
|
new NestJobOptions(maxPlates: 100,
|
||||||
items,
|
salvageRate: plateOptions?.Count > 0 ? form.SalvageRate : 0,
|
||||||
progressForm,
|
minimumSalvageDimension: form.MinRemnantSize));
|
||||||
progress,
|
|
||||||
nestingCts.Token,
|
|
||||||
plateOptions,
|
|
||||||
salvageRate,
|
|
||||||
minRemnantSize
|
|
||||||
);
|
|
||||||
else
|
|
||||||
await RunAutoNestAsync(
|
|
||||||
items,
|
|
||||||
progressForm,
|
|
||||||
progress,
|
|
||||||
nestingCts.Token,
|
|
||||||
plateOptions,
|
|
||||||
salvageRate,
|
|
||||||
partFirstMode,
|
|
||||||
sortOrder,
|
|
||||||
minRemnantSize,
|
|
||||||
allowPlateCreation
|
|
||||||
);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
activeForm.PlateView.ClearPreviewParts();
|
|
||||||
MessageBox.Show(
|
|
||||||
$"Nesting error: {ex.Message}",
|
|
||||||
"Error",
|
|
||||||
MessageBoxButtons.OK,
|
|
||||||
MessageBoxIcon.Error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
activeForm.PlateView.ActiveWorkArea = null;
|
|
||||||
progressForm.Close();
|
|
||||||
SetNestingLockout(false);
|
|
||||||
nestingCts.Dispose();
|
|
||||||
nestingCts = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task RunAutoNestAsync(
|
async Task<NestPipelineResult> SolveAsync()
|
||||||
List<NestItem> items,
|
|
||||||
NestProgressForm progressForm,
|
|
||||||
IProgress<NestProgress> progress,
|
|
||||||
CancellationToken token,
|
|
||||||
List<PlateOption> plateOptions = null,
|
|
||||||
double salvageRate = 0.5,
|
|
||||||
bool partFirstMode = false,
|
|
||||||
PartSortOrder sortOrder = PartSortOrder.BoundingBoxArea,
|
|
||||||
double minRemnantSize = 12.0,
|
|
||||||
bool allowPlateCreation = true
|
|
||||||
)
|
|
||||||
{
|
|
||||||
if (partFirstMode)
|
|
||||||
{
|
|
||||||
var existingPlates = new List<Plate>();
|
|
||||||
for (var i = 0; i < activeForm.Nest.Plates.Count; i++)
|
|
||||||
{
|
{
|
||||||
var p = activeForm.Nest.Plates[i];
|
try
|
||||||
if (p.Parts.Count > 0)
|
|
||||||
existingPlates.Add(p);
|
|
||||||
}
|
|
||||||
|
|
||||||
var template = activeForm.PlateView.Plate;
|
|
||||||
|
|
||||||
var nestOptions = new MultiPlateNestOptions
|
|
||||||
{
|
|
||||||
Template = template,
|
|
||||||
PlateOptions = plateOptions,
|
|
||||||
SalvageRate = salvageRate,
|
|
||||||
SortOrder = sortOrder,
|
|
||||||
MinRemnantSize = minRemnantSize,
|
|
||||||
AllowPlateCreation = allowPlateCreation,
|
|
||||||
Strategy = EngineSelection.FillStrategy,
|
|
||||||
};
|
|
||||||
|
|
||||||
var result = await Task.Run(() =>
|
|
||||||
MultiPlateNester.Nest(items, nestOptions, existingPlates, progress, token)
|
|
||||||
);
|
|
||||||
|
|
||||||
foreach (var pr in result.Plates)
|
|
||||||
{
|
|
||||||
if (pr.IsNew)
|
|
||||||
{
|
{
|
||||||
var plate = GetOrCreatePlate();
|
return await Task.Run(() => NestPipeline.Run(request, jobProgress, cts.Token));
|
||||||
plate.Size = pr.Plate.Size;
|
}
|
||||||
plate.Parts.AddRange(pr.Parts);
|
finally
|
||||||
|
{
|
||||||
|
receivingProgress = false;
|
||||||
|
progressForm.ShowCompleted();
|
||||||
|
progressForm.Close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
activeForm.Nest.UpdateDrawingQuantities();
|
var solve = SolveAsync();
|
||||||
progressForm.ShowCompleted();
|
// Owned modal progress holds drawings/plates stable through solve and cancellation.
|
||||||
return;
|
if (!solve.IsCompleted)
|
||||||
}
|
progressForm.ShowDialog(this);
|
||||||
|
var result = await solve;
|
||||||
|
cts.Token.ThrowIfCancellationRequested();
|
||||||
|
target.PlateView.ClearPreviewParts();
|
||||||
|
target.PlateView.ActiveWorkArea = null;
|
||||||
|
|
||||||
const int maxPlates = 100;
|
var allowInvalid = false;
|
||||||
|
if (!result.IsValid)
|
||||||
for (var plateIndex = 0; plateIndex < maxPlates; plateIndex++)
|
|
||||||
{
|
|
||||||
var remaining = items.Where(i => i.Quantity > 0).ToList();
|
|
||||||
|
|
||||||
if (remaining.Count == 0 || token.IsCancellationRequested)
|
|
||||||
break;
|
|
||||||
|
|
||||||
var plate = GetOrCreatePlate();
|
|
||||||
|
|
||||||
var placed = await NestSinglePlateAsync(
|
|
||||||
plate,
|
|
||||||
plateIndex,
|
|
||||||
remaining,
|
|
||||||
progressForm,
|
|
||||||
progress,
|
|
||||||
token,
|
|
||||||
plateOptions,
|
|
||||||
salvageRate
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!placed)
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
activeForm.Nest.UpdateDrawingQuantities();
|
|
||||||
progressForm.ShowCompleted();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whole-job path for StockLadder and Engines/ plug-ins: the engine owns plate count and
|
|
||||||
/// size selection, reports NestJobProgress into the progress form, and its result is
|
|
||||||
/// committed onto empty or new plates. Cancellation discards the run (engines throw).
|
|
||||||
/// </summary>
|
|
||||||
private async Task RunJobEngineAsync(
|
|
||||||
string engineName,
|
|
||||||
List<NestItem> items,
|
|
||||||
NestProgressForm progressForm,
|
|
||||||
IProgress<NestProgress> progress,
|
|
||||||
CancellationToken token,
|
|
||||||
List<PlateOption> plateOptions,
|
|
||||||
double salvageRate,
|
|
||||||
double minRemnantSize
|
|
||||||
)
|
|
||||||
{
|
|
||||||
const int maxPlates = 100;
|
|
||||||
|
|
||||||
var engine = NestingEngineRegistry.Create(engineName);
|
|
||||||
var job = JobEngineNest.BuildJob(
|
|
||||||
items,
|
|
||||||
activeForm.PlateView.Plate,
|
|
||||||
plateOptions,
|
|
||||||
salvageRate,
|
|
||||||
minRemnantSize,
|
|
||||||
maxPlates,
|
|
||||||
out var drawingsByPartId
|
|
||||||
);
|
|
||||||
var jobProgress = JobEngineNest.CreateProgress(engineName, progress);
|
|
||||||
|
|
||||||
NestJobResult result;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
result = await Task.Run(() => engine.Solve(job, jobProgress, token));
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException) when (token.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
activeForm.PlateView.ClearPreviewParts();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
activeForm.PlateView.ClearPreviewParts();
|
|
||||||
|
|
||||||
foreach (var sheet in result.Plates)
|
|
||||||
{
|
|
||||||
var parts = JobEngineNest.CreateParts(sheet, drawingsByPartId);
|
|
||||||
if (parts.Count == 0)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var plate = GetOrCreatePlate();
|
|
||||||
plate.Size = sheet.Stock.Size;
|
|
||||||
plate.Parts.AddRange(parts);
|
|
||||||
}
|
|
||||||
|
|
||||||
activeForm.PlateView.Invalidate();
|
|
||||||
activeForm.Nest.UpdateDrawingQuantities();
|
|
||||||
progressForm.ShowCompleted();
|
|
||||||
|
|
||||||
if (result.Status != NestJobStatus.Complete)
|
|
||||||
MessageBox.Show(
|
|
||||||
$"{engineName} could not place every part ({result.StopReason}).",
|
|
||||||
"Auto Nest",
|
|
||||||
MessageBoxButtons.OK,
|
|
||||||
MessageBoxIcon.Information
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Plate GetOrCreatePlate()
|
|
||||||
{
|
|
||||||
var plate = activeForm.PlateManager.GetOrCreateEmpty();
|
|
||||||
activeForm.PlateManager.LoadLast();
|
|
||||||
return plate;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<bool> NestSinglePlateAsync(
|
|
||||||
Plate plate,
|
|
||||||
int plateIndex,
|
|
||||||
List<NestItem> items,
|
|
||||||
NestProgressForm progressForm,
|
|
||||||
IProgress<NestProgress> progress,
|
|
||||||
CancellationToken token,
|
|
||||||
List<PlateOption> plateOptions = null,
|
|
||||||
double salvageRate = 0.5
|
|
||||||
)
|
|
||||||
{
|
|
||||||
List<Part> nestParts;
|
|
||||||
|
|
||||||
if (plateOptions != null && plateOptions.Count > 0)
|
|
||||||
{
|
|
||||||
var result = await Task.Run(() =>
|
|
||||||
PlateOptimizer.Optimize(
|
|
||||||
items,
|
|
||||||
plateOptions,
|
|
||||||
salvageRate,
|
|
||||||
plate,
|
|
||||||
progress,
|
|
||||||
token,
|
|
||||||
EngineSelection.FillStrategy
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
|
||||||
result == null
|
|
||||||
|| result.Parts.Count == 0
|
|
||||||
|| (token.IsCancellationRequested && !progressForm.Accepted)
|
|
||||||
)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
plate.Size = new Geometry.Size(result.ChosenSize.Width, result.ChosenSize.Length);
|
|
||||||
nestParts = result.Parts;
|
|
||||||
|
|
||||||
// Deduct placed quantities — the optimizer clones items internally
|
|
||||||
// so the originals are untouched after dry runs.
|
|
||||||
foreach (var item in items)
|
|
||||||
{
|
{
|
||||||
var placed = nestParts.Count(p => p.BaseDrawing.Name == item.Drawing.Name);
|
using var review = new NestValidationForm(result.Violations, result.CanKeep);
|
||||||
item.Quantity = System.Math.Max(0, item.Quantity - placed);
|
if (review.ShowDialog(this) != DialogResult.OK)
|
||||||
|
return;
|
||||||
|
allowInvalid = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var applied = NestPipelineCommit.ApplyToEmptyPlates(result, target.PlateManager,
|
||||||
|
allowInvalid, cts.Token);
|
||||||
|
if (applied.Count > 0)
|
||||||
|
target.PlateManager.LoadAt(target.Nest.Plates.IndexOf(applied[0]));
|
||||||
|
if (plateOptions?.Count > 0)
|
||||||
|
{
|
||||||
|
target.Nest.PlateOptions = plateOptions;
|
||||||
|
target.Nest.SalvageRate = form.SalvageRate;
|
||||||
|
}
|
||||||
|
target.PlateView.Invalidate();
|
||||||
|
if (allowInvalid)
|
||||||
|
{
|
||||||
|
target.OverlapDisplay = OverlapDisplayMode.Both;
|
||||||
|
await target.CheckOverlapsAsync();
|
||||||
|
}
|
||||||
|
if (result.Status != NestJobStatus.Complete)
|
||||||
|
MessageBox.Show(this, $"{engineName} could not place every part ({result.StopReason}).",
|
||||||
|
"Auto Nest", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||||
}
|
}
|
||||||
else
|
catch (OperationCanceledException) when (cts.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
// Same preview flow as before: fill the current plate's remaining demand with the
|
// Stop discards the entire proposal, including engines that ignore cancellation.
|
||||||
// selected strategy, then commit the returned parts. App-scoped selection — the
|
}
|
||||||
// process-global engine registry is never consulted.
|
catch (Exception ex)
|
||||||
nestParts = await Task.Run(() =>
|
{
|
||||||
PlateFillService.Nest(
|
MessageBox.Show(this, $"Nesting error: {ex.Message}", "Auto Nest",
|
||||||
EngineSelection.FillStrategy,
|
MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||||
plate,
|
}
|
||||||
items,
|
finally
|
||||||
plateIndex,
|
{
|
||||||
progress,
|
receivingProgress = false;
|
||||||
token
|
if (!target.IsDisposed)
|
||||||
)
|
{
|
||||||
);
|
target.PlateView.ClearPreviewParts();
|
||||||
|
target.PlateView.ActiveWorkArea = null;
|
||||||
|
}
|
||||||
|
SetNestingLockout(false);
|
||||||
|
nestingCts = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
activeForm.PlateView.ClearPreviewParts();
|
|
||||||
|
|
||||||
if (nestParts.Count == 0 || (token.IsCancellationRequested && !progressForm.Accepted))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
plate.Parts.AddRange(nestParts);
|
|
||||||
activeForm.PlateView.Invalidate();
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SequenceAllPlates_Click(object sender, EventArgs e)
|
private void SequenceAllPlates_Click(object sender, EventArgs e)
|
||||||
|
|||||||
@@ -25,6 +25,20 @@ namespace OpenNest.Forms
|
|||||||
private readonly System.Windows.Forms.Timer fadeTimer;
|
private readonly System.Windows.Forms.Timer fadeTimer;
|
||||||
private readonly Dictionary<Label, (int remaining, Color flashColor)> fadeCounters = new();
|
private readonly Dictionary<Label, (int remaining, Color flashColor)> fadeCounters = new();
|
||||||
private bool hasReceivedProgress;
|
private bool hasReceivedProgress;
|
||||||
|
private bool completed;
|
||||||
|
|
||||||
|
/// <summary>Keep the owner disabled until a whole-job worker finishes after Stop/close.</summary>
|
||||||
|
private bool holdOpenUntilCompleted;
|
||||||
|
public bool HoldOpenUntilCompleted
|
||||||
|
{
|
||||||
|
get => holdOpenUntilCompleted;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
holdOpenUntilCompleted = value;
|
||||||
|
if (value)
|
||||||
|
stopButton.Enabled = true; // Plug-ins need not report progress before cancellation.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public bool Accepted { get; private set; }
|
public bool Accepted { get; private set; }
|
||||||
|
|
||||||
@@ -94,6 +108,7 @@ namespace OpenNest.Forms
|
|||||||
|
|
||||||
public void ShowCompleted()
|
public void ShowCompleted()
|
||||||
{
|
{
|
||||||
|
completed = true;
|
||||||
if (IsDisposed || !IsHandleCreated)
|
if (IsDisposed || !IsHandleCreated)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -143,13 +158,19 @@ namespace OpenNest.Forms
|
|||||||
|
|
||||||
protected override void OnFormClosing(FormClosingEventArgs e)
|
protected override void OnFormClosing(FormClosingEventArgs e)
|
||||||
{
|
{
|
||||||
|
if (HoldOpenUntilCompleted && !completed)
|
||||||
|
{
|
||||||
|
cts.Cancel();
|
||||||
|
e.Cancel = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
fadeTimer.Stop();
|
fadeTimer.Stop();
|
||||||
fadeTimer.Dispose();
|
fadeTimer.Dispose();
|
||||||
elapsedTimer.Stop();
|
elapsedTimer.Stop();
|
||||||
elapsedTimer.Dispose();
|
elapsedTimer.Dispose();
|
||||||
stopwatch.Stop();
|
stopwatch.Stop();
|
||||||
|
|
||||||
if (!cts.IsCancellationRequested)
|
if (!completed && !cts.IsCancellationRequested)
|
||||||
cts.Cancel();
|
cts.Cancel();
|
||||||
|
|
||||||
base.OnFormClosing(e);
|
base.OnFormClosing(e);
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Drawing;
|
||||||
|
using System.Windows.Forms;
|
||||||
|
|
||||||
|
namespace OpenNest.Forms;
|
||||||
|
|
||||||
|
/// <summary>A fresh, explicit decision for each invalid automatic nesting proposal.</summary>
|
||||||
|
public sealed class NestValidationForm : Form
|
||||||
|
{
|
||||||
|
public NestValidationForm(IReadOnlyList<string> violations, bool canKeep)
|
||||||
|
{
|
||||||
|
Text = "Auto Nest — validation problems";
|
||||||
|
StartPosition = FormStartPosition.CenterParent;
|
||||||
|
ClientSize = new Size(740, 440);
|
||||||
|
MinimizeBox = false;
|
||||||
|
MaximizeBox = false;
|
||||||
|
ShowInTaskbar = false;
|
||||||
|
var summary = new Label
|
||||||
|
{
|
||||||
|
Dock = DockStyle.Top,
|
||||||
|
Height = 70,
|
||||||
|
Padding = new Padding(10),
|
||||||
|
Text = canKeep
|
||||||
|
? "This layout failed validation. Discard leaves your plates unchanged. Keep anyway applies the entire layout and enables Overlap Check.\nOverlap Check highlights material overlaps; spacing, stock and rotation warnings remain listed here."
|
||||||
|
: "The engine returned malformed placements. No layout can be kept. Discard leaves your plates unchanged.",
|
||||||
|
};
|
||||||
|
var report = new TextBox
|
||||||
|
{
|
||||||
|
Dock = DockStyle.Fill,
|
||||||
|
Multiline = true,
|
||||||
|
ReadOnly = true,
|
||||||
|
ScrollBars = ScrollBars.Both,
|
||||||
|
WordWrap = false,
|
||||||
|
Text = string.Join("\r\n\r\n", violations),
|
||||||
|
};
|
||||||
|
var buttons = new FlowLayoutPanel
|
||||||
|
{
|
||||||
|
Dock = DockStyle.Bottom,
|
||||||
|
Height = 48,
|
||||||
|
FlowDirection = FlowDirection.RightToLeft,
|
||||||
|
Padding = new Padding(6),
|
||||||
|
};
|
||||||
|
var discard = new Button { Text = "Discard", AutoSize = true, DialogResult = DialogResult.Cancel };
|
||||||
|
var keep = new Button
|
||||||
|
{
|
||||||
|
Text = "Keep anyway",
|
||||||
|
AutoSize = true,
|
||||||
|
Enabled = canKeep,
|
||||||
|
DialogResult = DialogResult.OK,
|
||||||
|
};
|
||||||
|
buttons.Controls.Add(discard);
|
||||||
|
buttons.Controls.Add(keep);
|
||||||
|
Controls.Add(report);
|
||||||
|
Controls.Add(summary);
|
||||||
|
Controls.Add(buttons);
|
||||||
|
AcceptButton = discard;
|
||||||
|
CancelButton = discard;
|
||||||
|
ActiveControl = discard;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# Automatic nesting pipeline
|
||||||
|
|
||||||
|
`NestPipeline.Run` is the shared whole-job boundary in `OpenNest.Engine`:
|
||||||
|
|
||||||
|
1. Snapshot caller requirements and stock into a `NestJob`.
|
||||||
|
2. Resolve the selected name through `NestingEngineRegistry` (built-ins and plug-ins use the same path).
|
||||||
|
3. Run the engine without mutating caller drawings or plates.
|
||||||
|
4. Check returned placements with the independent `NestLayoutCheck` used by the benchmark.
|
||||||
|
5. Bind a representable result back to the caller's drawing references; the caller decides whether to commit it.
|
||||||
|
|
||||||
|
`NestStockBuilder.FromTemplate` snapshots sheet options and spacing settings for multiple-sheet jobs. `SinglePlate` offers exactly one physical sheet, regardless of the legacy plate repeat quantity. `NestPipelineCommit.ApplyToEmptyPlates` applies accepted desktop proposals to empty/new plates with the checked size, quadrant, spacing and quantity-one semantics. It never fills occupied plates.
|
||||||
|
|
||||||
|
## Desktop Auto Nest
|
||||||
|
|
||||||
|
Every engine selected in **Nest > Auto Nest** uses the pipeline. Part-First and its sorting controls are removed. Use interactive Fill Area/remnant tools for leftover space on occupied sheets; these are not whole-job nesting.
|
||||||
|
|
||||||
|
Auto Nest starts on empty/new sheets and does not change existing populated plates. Stock Options offers sheet sizes and salvage settings; minimum salvage size is part of that section, not a Part-First option. The maximum remains 100 physical sheets per run.
|
||||||
|
|
||||||
|
Progress is owned and modal so the input drawings cannot be edited while a worker uses them. Stop or closing progress cancels and discards the whole proposal. The dialog waits for the worker to finish; no engine has an Accept-early button on this path. A plug-in that ignores the cancellation token cannot commit its late result.
|
||||||
|
|
||||||
|
Every invalid result shows the validation report before any plates are changed:
|
||||||
|
|
||||||
|
- **Discard** is the default button and the Escape action. No proposed parts or sheets are applied.
|
||||||
|
- **Keep anyway** requires an explicit click and keeps the entire representable proposal, then enables Overlap Check.
|
||||||
|
- Malformed output (unknown requirements or nonfinite poses) is not keepable. It cannot be faithfully represented as the proposed layout; no partial/truncated proposal is offered.
|
||||||
|
|
||||||
|
Overlap Check displays material overlaps, not every spacing/stock/rotation failure in the report. A layout passing validation may still be incomplete; completeness and stop reason are separate from geometric validity. This check does not replace pre-post CNC verification.
|
||||||
|
|
||||||
|
The selected engine is saved in `%APPDATA%\OpenNest\engine-selection.json`.
|
||||||
|
Selection is restored after plug-ins load. If the saved engine is unavailable,
|
||||||
|
Default is selected and the status bar reports the fallback; startup does not
|
||||||
|
replace the saved missing-engine preference.
|
||||||
|
|
||||||
|
## Integration constraints
|
||||||
|
|
||||||
|
Callers must hold drawings and target state stable from snapshot through attachment. Do not append whole-job placements to an occupied target or flatten multiple returned sheets onto one plate: that would commit a layout different from the one checked. Do not trust arbitrary engine fulfillment metadata as a substitute for counting returned placements.
|
||||||
|
|
||||||
|
Interactive fills are outside this contract and still use `PlateFillService`. The benchmark invokes the same independent validator. Console/MCP/API migration is a separate adoption step; the existence of the pipeline does not imply every front end already calls it.
|
||||||
Reference in New Issue
Block a user