feat(ui): route Auto Nest through whole-job engines
StockLadder and Engines/ plug-ins only implement INestingEngine.Solve, so selecting them in Auto Nest had no path to run. MainForm now solves the whole job through JobEngineNest when the selected engine is not a built-in fill strategy, feeding NestJobProgress into NestProgressForm and binding the result poses back onto the nest's own drawings. Whole-job engines throw on cancel rather than returning a partial layout, so the progress form hides Accept for these runs. Built-in strategies keep the existing per-plate fill path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -102,6 +102,7 @@ MCP server for Claude Code integration. Exposes nesting operations as MCP tools
|
||||
### OpenNest (WinForms WinExe, depends on Core + Engine + IO)
|
||||
The UI application with MDI interface.
|
||||
|
||||
- **Auto Nest engine routing**: when the selected engine is not a built-in fill strategy (`EngineSelection.IsFillStrategy` is false, i.e. StockLadder or an `Engines/` plug-in), `MainForm.RunJobEngineAsync` solves the whole job through `INestingEngine.Solve`. `JobEngineNest` builds the `NestJob` from the auto-nest items and either the plate options or the current plate, and it converts `NestJobProgress` for `NestProgressForm`: an engine's `LegacyProgress` passes through, and otherwise the stage and committed counts become the description. It then binds the result poses back onto the nest's own drawings. Whole-job engines throw on cancel, so the progress form hides Accept (`AllowAccept = false`) and Stop discards the run. Built-in strategies keep the existing per-plate fill path.
|
||||
- **Forms/**: `MainForm` (MDI parent), `EditNestForm` (MDI child per nest), `SplitDrawingForm` (split oversized drawings into smaller pieces, launched from CadConverterForm), plus dialogs for plate editing, auto-nesting, DXF conversion, cut parameters, etc.
|
||||
- **Controls/**: `PlateView` (2D plate renderer with zoom/pan, supports temporary preview parts), `DrawingListBox`, `DrawControl`, `QuadrantSelect`.
|
||||
- **Actions/**: User interaction modes — `ActionSelect`, `ActionClone`, `ActionFillArea`, `ActionSelectArea`, `ActionZoomWindow`, `ActionSetSequence`, `ActionCutOff`.
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenNest.Engine;
|
||||
using OpenNest.Engine.Jobs;
|
||||
using OpenNest.Engine.Jobs.Adapters;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Forms
|
||||
{
|
||||
/// <summary>
|
||||
/// Desktop adapter for whole-job engines (StockLadder and Engines/ plug-ins): builds the
|
||||
/// NestJob from auto-nest items, translates NestJobProgress for NestProgressForm, and maps
|
||||
/// result poses back onto the nest's own drawings.
|
||||
/// </summary>
|
||||
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>
|
||||
/// 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
|
||||
/// description. Committed counts are authoritative only after PlateCommitted.
|
||||
/// </summary>
|
||||
public static IProgress<NestJobProgress> CreateProgress(
|
||||
string engineName,
|
||||
IProgress<NestProgress> progress
|
||||
) => new ProgressAdapter(engineName, progress);
|
||||
|
||||
public static string Describe(string engineName, NestJobProgress value) =>
|
||||
value.Stage == NestJobStage.PlateCommitted
|
||||
? $"{engineName}: committed plate {value.CommittedPlates} ({value.CommittedParts} parts placed)"
|
||||
: $"{engineName}: evaluating plate {WorkingPlate(value)} on stock {value.StockId}";
|
||||
|
||||
/// <summary>One-based plate under evaluation; nesters that do not know it report -1.</summary>
|
||||
private static int WorkingPlate(NestJobProgress value) =>
|
||||
value.PlateIndex >= 0 ? value.PlateIndex + 1 : value.CommittedPlates + 1;
|
||||
|
||||
private sealed class ProgressAdapter(string engineName, IProgress<NestProgress> progress)
|
||||
: IProgress<NestJobProgress>
|
||||
{
|
||||
public void Report(NestJobProgress value)
|
||||
{
|
||||
if (value == null)
|
||||
return;
|
||||
|
||||
if (value.LegacyProgress != null)
|
||||
{
|
||||
progress.Report(value.LegacyProgress);
|
||||
return;
|
||||
}
|
||||
|
||||
progress.Report(
|
||||
new NestProgress
|
||||
{
|
||||
Phase = NestPhase.Custom,
|
||||
PlateNumber =
|
||||
value.Stage == NestJobStage.PlateCommitted
|
||||
? value.CommittedPlates
|
||||
: WorkingPlate(value),
|
||||
Description = Describe(engineName, value),
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1042,6 +1042,11 @@ namespace OpenNest.Forms
|
||||
var progressForm = new NestProgressForm(nestingCts, showPlateRow: true);
|
||||
progressForm.PreviewPlate = CreatePreviewPlate(activeForm.PlateView.Plate);
|
||||
|
||||
var jobEngineName = EngineSelection.IsFillStrategy(EngineSelection.EngineName)
|
||||
? null
|
||||
: EngineSelection.EngineName;
|
||||
progressForm.AllowAccept = jobEngineName == null;
|
||||
|
||||
var progress = new Progress<NestProgress>(p =>
|
||||
{
|
||||
progressForm.UpdateProgress(p);
|
||||
@@ -1058,6 +1063,18 @@ namespace OpenNest.Forms
|
||||
|
||||
try
|
||||
{
|
||||
if (jobEngineName != null)
|
||||
await RunJobEngineAsync(
|
||||
jobEngineName,
|
||||
items,
|
||||
progressForm,
|
||||
progress,
|
||||
nestingCts.Token,
|
||||
plateOptions,
|
||||
salvageRate,
|
||||
minRemnantSize
|
||||
);
|
||||
else
|
||||
await RunAutoNestAsync(
|
||||
items,
|
||||
progressForm,
|
||||
@@ -1176,6 +1193,73 @@ namespace OpenNest.Forms
|
||||
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(progressForm);
|
||||
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(NestProgressForm progressForm)
|
||||
{
|
||||
var plate = activeForm.PlateManager.GetOrCreateEmpty();
|
||||
|
||||
@@ -28,6 +28,16 @@ namespace OpenNest.Forms
|
||||
|
||||
public bool Accepted { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// False hides "Accept" for solvers that cannot return a partial result on cancellation
|
||||
/// (whole-job engines throw instead).
|
||||
/// </summary>
|
||||
public bool AllowAccept
|
||||
{
|
||||
get => acceptButton.Visible;
|
||||
set => acceptButton.Visible = value;
|
||||
}
|
||||
|
||||
public Plate PreviewPlate
|
||||
{
|
||||
get => previewPlateView.Plate;
|
||||
|
||||
Reference in New Issue
Block a user