Files
aj be5fef6875 fix(release): enforce one finish per commit and keep real unplaced reasons
Delta review fixes:

- ReleaseRenest throws when a commit does not call finish, or calls it
  twice: either would leave ledger plates uncoded or count a sheet twice
  for later nests of the run. The ledger sheet count is the codes given.
- A stage whose layout is not kept now lists the copies its layout
  missed with their own reasons (a part no sheet fits stays "does not
  fit"); only the copies it placed are "the proposal cannot be kept".
- A valid ledger layout that places nothing says "Auto Nest placed
  nothing on it" rather than claiming no part fits.
- ReleaseLedgerOffer rejects null rows; docs list every ledger note.
2026-11-03 18:03:24 -05:00

118 lines
5.4 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using OpenNest.Data.MaterialLedger;
using OpenNest.Engine;
using OpenNest.Engine.Jobs;
namespace OpenNest.Desktop
{
/// <summary>
/// What Make Nests' renest step kept, its note and the ledger codes' locations.
/// </summary>
public sealed record ReleaseRenestResult(bool Nested, string Note,
IReadOnlyDictionary<string, string> LedgerLocations);
/// <summary>
/// Make Nests' Auto Nest of one released nest: on the run's MaterialLedger stock first,
/// then on the nest's plate defaults (<see cref="NestRenest.RunStagedAsync"/>), with the
/// ledger codes given to the plates inside each commit, so they are part of its undo step.
/// Shared by the headless and the live (main window) release.
/// </summary>
public static class ReleaseRenest
{
/// <summary>
/// Attaches a kept result's sheets to the nest's plates and returns them. It must call
/// <c>finish</c> once with those plates before its undo step closes; that gives them
/// their ledger codes. Not calling it, or calling it twice, throws.
/// </summary>
public delegate IReadOnlyList<Plate> Commit(NestPipelineResult result,
Action<IReadOnlyList<Plate>> finish);
/// <summary>
/// Nests <paramref name="nest"/>, which has no parts placed, on <paramref name="ledger"/>'s
/// stock (null: the plate defaults only) and then its plate defaults. The ledger is not
/// read when nothing has a quantity to nest.
/// </summary>
public static async Task<ReleaseRenestResult> RunAsync(Nest nest, ReleaseLedgerStock ledger,
string engineName, double minimumSalvageDimension,
Func<NestPipelineRequest, Task<NestPipelineResult>> solve, Commit commit,
CancellationToken token)
{
ArgumentNullException.ThrowIfNull(nest);
ArgumentNullException.ThrowIfNull(solve);
ArgumentNullException.ThrowIfNull(commit);
var wanted = nest.Drawings.Any(d => !d.IsCutOff && d.Quantity.Remaining > 0);
var offer = ledger != null && wanted ? await ledger.OfferAsync(nest, token) : null;
var locations = new Dictionary<string, string>(StringComparer.Ordinal);
var sheets = 0;
var renested = await NestRenest.RunStagedAsync(nest, engineName, offer?.Options,
minimumSalvageDimension, solve,
result =>
{
var finished = false;
var plates = commit(result, applied =>
{
if (finished)
throw new InvalidOperationException(
"A release commit finished its plates twice.");
finished = true;
if (offer == null)
return;
var stock = NestPipelineCommit.PairWithStock(result, applied)
.Select(p => (p.Plate, p.Stock.Id))
.ToList();
foreach (var (code, location) in ledger.AssignCodes(nest, offer, stock))
locations[code] = location;
sheets += stock.Sum(p => p.Plate.StockItemCodes.Count);
});
if (!finished)
throw new InvalidOperationException(
"A release commit did not finish its plates, so they have no codes.");
return plates;
},
token);
var ledgerNote = offer == null
? null
: ReleaseLedgerStock.Note(offer, sheets, renested.FirstStockNotKept);
return new ReleaseRenestResult(renested.Nested, Join(renested.Note, ledgerNote),
locations);
}
/// <summary>
/// <see cref="RunAsync"/> for a nest no one is looking at: every plate is removed, the
/// layouts are solved on the calling thread and attached as new plates.
/// </summary>
public static Task<ReleaseRenestResult> RunHeadlessAsync(Nest nest,
ReleaseLedgerStock ledger, INestingEngine engine, string engineName,
double minimumSalvageDimension,
CancellationToken token)
{
ArgumentNullException.ThrowIfNull(nest);
ArgumentNullException.ThrowIfNull(engine);
token.ThrowIfCancellationRequested();
nest.Plates.Clear();
nest.UpdateDrawingQuantities();
return RunAsync(nest, ledger, engineName, minimumSalvageDimension,
request => Task.FromResult(
NestPipeline.Run(engine, engineName, request, null, token)),
(result, finish) =>
{
var plates = NestPipelineCommit.ApplyToPlates(result, nest.CreatePlate,
token: token);
finish(plates);
return plates;
},
token);
}
/// <summary>Auto Nest's note, then what MaterialLedger stock the nest used.</summary>
public static string Join(string renestNote, string ledgerNote) =>
string.Join(" ", new[] { renestNote, ledgerNote }.Where(n => !string.IsNullOrEmpty(n)));
}
}