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)
|
### OpenNest (WinForms WinExe, depends on Core + Engine + IO)
|
||||||
The UI application with MDI interface.
|
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.
|
- **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`.
|
- **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`.
|
- **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);
|
var progressForm = new NestProgressForm(nestingCts, showPlateRow: true);
|
||||||
progressForm.PreviewPlate = CreatePreviewPlate(activeForm.PlateView.Plate);
|
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 =>
|
var progress = new Progress<NestProgress>(p =>
|
||||||
{
|
{
|
||||||
progressForm.UpdateProgress(p);
|
progressForm.UpdateProgress(p);
|
||||||
@@ -1058,6 +1063,18 @@ namespace OpenNest.Forms
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
if (jobEngineName != null)
|
||||||
|
await RunJobEngineAsync(
|
||||||
|
jobEngineName,
|
||||||
|
items,
|
||||||
|
progressForm,
|
||||||
|
progress,
|
||||||
|
nestingCts.Token,
|
||||||
|
plateOptions,
|
||||||
|
salvageRate,
|
||||||
|
minRemnantSize
|
||||||
|
);
|
||||||
|
else
|
||||||
await RunAutoNestAsync(
|
await RunAutoNestAsync(
|
||||||
items,
|
items,
|
||||||
progressForm,
|
progressForm,
|
||||||
@@ -1176,6 +1193,73 @@ namespace OpenNest.Forms
|
|||||||
progressForm.ShowCompleted();
|
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)
|
private Plate GetOrCreatePlate(NestProgressForm progressForm)
|
||||||
{
|
{
|
||||||
var plate = activeForm.PlateManager.GetOrCreateEmpty();
|
var plate = activeForm.PlateManager.GetOrCreateEmpty();
|
||||||
|
|||||||
@@ -28,6 +28,16 @@ namespace OpenNest.Forms
|
|||||||
|
|
||||||
public bool Accepted { get; private set; }
|
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
|
public Plate PreviewPlate
|
||||||
{
|
{
|
||||||
get => previewPlateView.Plate;
|
get => previewPlateView.Plate;
|
||||||
|
|||||||
Reference in New Issue
Block a user