mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-27 03:51:05 -04:00
feat(desktop): show Auto Nest as a sheet strip with a Keep/Discard review
Auto Nest no longer draws parts on the plate view before they are kept: its result is a set of sheets, possibly of several sizes, not parts on the current plate. Ghost parts stay with interactive Fill and remnant tools. - NestJobStage.PlanImproved carries an engine's best whole-job plan so far; Irregular reports one whenever a variant or the tail re-plan beats it. - NestPipeline adds PlanSheets: the plan on detached drawings built from the job geometry (with the caller's names and colors), so previews never touch caller drawings or quantities. - SheetStripControl draws every sheet side by side at one scale, repeats once with their count, captioned with size, parts and utilization. - The progress window shows "Best plan so far"; a review window shows the final sheets, totals, cost summary and unplaced parts before commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a2fb945669
commit
cd2bea054b
13 files changed
+692
-47
No files matched your search
@@ -1,9 +1,17 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.Engine.Jobs;
|
||||
|
||||
public enum NestJobStage
|
||||
{
|
||||
EvaluatingCandidate,
|
||||
PlateCommitted,
|
||||
|
||||
/// <summary>
|
||||
/// The engine's best whole-job plan so far, in <see cref="NestJobProgress.Plan"/>. Not
|
||||
/// validated and possibly replaced by a later plan; never counts as committed.
|
||||
/// </summary>
|
||||
PlanImproved,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -16,5 +24,14 @@ public sealed record NestJobProgress(
|
||||
int PlateIndex,
|
||||
int CommittedPlates,
|
||||
int CommittedParts,
|
||||
NestProgress LegacyProgress = null
|
||||
);
|
||||
NestProgress LegacyProgress = null,
|
||||
NestJobResult Plan = null
|
||||
)
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="Plan"/> as displayable sheets, filled in by <see cref="NestPipeline"/> for
|
||||
/// <see cref="NestJobStage.PlanImproved"/> reports. Parts use detached drawings built from
|
||||
/// the job geometry, so they never touch the caller's drawings or quantities.
|
||||
/// </summary>
|
||||
public IReadOnlyList<ProposedPlate> PlanSheets { get; init; }
|
||||
}
|
||||
@@ -106,11 +106,71 @@ public static class NestPipeline
|
||||
{
|
||||
public void Report(NestJobProgress value)
|
||||
{
|
||||
if (value != null && value.Stage == NestJobStage.EvaluatingCandidate)
|
||||
if (value != null && value.Stage is NestJobStage.EvaluatingCandidate or NestJobStage.PlanImproved)
|
||||
inner.Report(value with { CommittedPlates = 0, CommittedParts = 0 });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fills <see cref="NestJobProgress.PlanSheets"/> on an engine's plan reports. Parts use
|
||||
/// detached drawings built from the solved job's geometry (bundled cutout frames included),
|
||||
/// one per part, so a preview never touches the caller's drawings or their quantities.
|
||||
/// </summary>
|
||||
private sealed class PlanProgress(IProgress<NestJobProgress> inner, NestJob engineJob,
|
||||
IReadOnlyDictionary<string, Drawing> callerDrawings, CancellationToken token)
|
||||
: IProgress<NestJobProgress>
|
||||
{
|
||||
private readonly Dictionary<string, NestJobPart> parts =
|
||||
engineJob.Parts.ToDictionary(p => p.Id, StringComparer.Ordinal);
|
||||
private readonly Dictionary<string, Drawing> drawings = new(StringComparer.Ordinal);
|
||||
|
||||
public void Report(NestJobProgress value)
|
||||
{
|
||||
if (value?.Stage == NestJobStage.PlanImproved && value.Plan != null && value.PlanSheets == null)
|
||||
{
|
||||
lock (drawings)
|
||||
{
|
||||
// A plan naming unknown parts has nothing to draw; validation reports it later.
|
||||
if (value.Plan.Plates.Any(s => s.Placements.Any(p => p.PartId == null || !parts.ContainsKey(p.PartId))))
|
||||
return;
|
||||
value = value with { PlanSheets = Sheets(value.Plan) };
|
||||
}
|
||||
}
|
||||
inner.Report(value);
|
||||
}
|
||||
|
||||
private IReadOnlyList<ProposedPlate> Sheets(NestJobResult plan)
|
||||
{
|
||||
var sheets = new List<ProposedPlate>();
|
||||
foreach (var group in RepeatedPlateGrouping.Create(plan.Plates, true, token))
|
||||
{
|
||||
var placed = new List<Part>(group.Sheet.Placements.Count);
|
||||
foreach (var pose in group.Sheet.Placements)
|
||||
{
|
||||
if (!drawings.TryGetValue(pose.PartId, out var drawing))
|
||||
{
|
||||
drawing = DrawingJobMapper.CreateDrawing(parts[pose.PartId]);
|
||||
// Borrow the caller's name and color for display; bundled frames keep their own.
|
||||
if (callerDrawings.TryGetValue(pose.PartId, out var caller))
|
||||
{
|
||||
drawing.Name = caller.Name;
|
||||
drawing.Color = caller.Color;
|
||||
}
|
||||
drawings[pose.PartId] = drawing;
|
||||
}
|
||||
// Same pose mapping as NestResultMaterializer: rotate in the snapshot frame, then move.
|
||||
var part = new Part(drawing);
|
||||
part.Rotate(pose.Rotation);
|
||||
part.Location = new Geometry.Vector(pose.X, pose.Y);
|
||||
part.UpdateBounds();
|
||||
placed.Add(part);
|
||||
}
|
||||
sheets.Add(new ProposedPlate(group.Sheet.PlateIndex, group.Sheet.Stock, placed) { Quantity = group.Quantity });
|
||||
}
|
||||
return sheets;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Resolves <see cref="NestPipelineRequest.EngineName"/> through
|
||||
/// <see cref="NestingEngineRegistry"/>; unknown names throw <see cref="NotSupportedException"/>.
|
||||
/// Engine cancellation propagates unchanged.</summary>
|
||||
@@ -263,9 +323,10 @@ public static class NestPipeline
|
||||
NestJobValidator.Validate(engineJob);
|
||||
|
||||
var clock = Stopwatch.StartNew();
|
||||
var engineProgress = progress == null ? null
|
||||
: new PlanProgress(prepass == null ? progress : new PreviewProgress(progress), engineJob, drawingsByPartId, token);
|
||||
var raw =
|
||||
engine.Solve(engineJob, prepass == null || progress == null
|
||||
? progress : new PreviewProgress(progress), token)
|
||||
engine.Solve(engineJob, engineProgress, token)
|
||||
?? throw new InvalidOperationException($"Engine '{engineName}' returned no result.");
|
||||
var solveTime = clock.Elapsed;
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
@@ -89,14 +89,29 @@ public sealed class IrregularNestingEngine : INestingEngine
|
||||
break;
|
||||
var plan = solver.Plan(demand, axis, beta);
|
||||
if (best == null || plan.IsBetterThan(best))
|
||||
{
|
||||
best = plan;
|
||||
ReportPlan(job, types, best, progress, token);
|
||||
}
|
||||
if (best.Unplaced == 0 && best.Sheets.Count == 0)
|
||||
break;
|
||||
}
|
||||
|
||||
// The last sheets hold the leftovers, which is where waste concentrates; re-plan them.
|
||||
best = solver.ImproveTail(best!, WorkBudget * 2);
|
||||
return BuildResult(job, types, best, progress, token);
|
||||
var improved = solver.ImproveTail(best!, WorkBudget * 2);
|
||||
if (!ReferenceEquals(improved, best))
|
||||
ReportPlan(job, types, improved, progress, token);
|
||||
return BuildResult(job, types, improved, progress, token);
|
||||
}
|
||||
|
||||
/// <summary>Reports a new best whole-job plan; reporting never changes the search.</summary>
|
||||
private static void ReportPlan(NestJob job, IReadOnlyList<PartType> types, Plan plan,
|
||||
IProgress<NestJobProgress>? progress, CancellationToken token)
|
||||
{
|
||||
if (progress == null)
|
||||
return;
|
||||
var result = BuildResult(job, types, plan, null, token);
|
||||
progress.Report(new NestJobProgress(NestJobStage.PlanImproved, string.Empty, -1, 0, 0, Plan: result));
|
||||
}
|
||||
|
||||
/// <summary>Exercises the production physical-tail lifecycle from a test-owned incumbent.
|
||||
|
||||
Reference in new issue
Block a user