using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace OpenNest.Engine.Jobs; /// /// What kept: whether a layout was committed, its physical sheets /// and utilization, the copies it left unplaced, and one operator-facing note. A staged run /// () lists every copy of a layout it did not keep, /// the ones that layout placed as ; /// lists the result's own unplaced copies. /// public sealed record NestRenestResult( bool Nested, int Sheets, double Utilization, IReadOnlyList Unplaced, string Note ) { /// /// Each committed plate with the engine stock id it was solved on, in commit order. /// public IReadOnlyList<(Plate Plate, string StockId)> Committed { get; init; } = Array.Empty<(Plate Plate, string StockId)>(); /// /// True when a staged run's first stock got a layout that was not valid or not keepable, /// so none of it was used (). /// public bool FirstStockNotKept { get; init; } } /// /// Auto Nest without the dialog, for a nest no one is looking at yet (a released drawing /// set): every plate is removed, then every drawing's required quantity is nested onto /// sheets from the nest's plate defaults and plate-size options through /// . A layout is committed only when it is valid and keepable; /// otherwise the nest is left without plates for the operator to run Auto Nest. Copies the /// layout leaves unplaced stay as remaining quantity. The caller owns the nest throughout. /// A caller showing the nest uses (or /// and ) to solve off its thread and commit on it. /// public static class NestRenest { /// The note left when no layout is kept. public const string NotKeptNote = "Auto Nest could not keep a layout: open it and run Auto Nest."; /// The note left when no drawing has a quantity to nest. public const string NothingToNestNote = "Nothing has a quantity to nest."; /// Most physical sheets one renest may use, as the Auto Nest dialog allows. public const int MaxPlates = 100; /// Renests with the engine registered as . public static NestRenestResult Run(Nest nest, string engineName, double minimumSalvageDimension = 0, IProgress progress = null, CancellationToken token = default) => Run(nest, NestingEngineRegistry.Create(engineName), engineName, minimumSalvageDimension, progress, token); /// /// Smallest offcut credited when the nest offers plate-size options, in the nest's units. /// public static NestRenestResult Run(Nest nest, INestingEngine engine, string engineName, double minimumSalvageDimension = 0, IProgress progress = null, CancellationToken token = default) { ArgumentNullException.ThrowIfNull(nest); ArgumentNullException.ThrowIfNull(engine); token.ThrowIfCancellationRequested(); nest.Plates.Clear(); nest.UpdateDrawingQuantities(); // Solved synchronously: the returned task has already completed. return RunStagedAsync(nest, engineName, null, minimumSalvageDimension, request => Task.FromResult( NestPipeline.Run(engine, engineName, request, progress, token)), kept => NestPipelineCommit.ApplyToPlates(kept, nest.CreatePlate, token: token), token) .GetAwaiter().GetResult(); } /// /// Nests every drawing's remaining quantity of , which has no parts /// placed yet, in up to two kept layouts: first on alone /// (finite sheets such as MaterialLedger stock, so they are used before anything else), /// then whatever is left on the plate defaults and plate-size options. A stage whose layout /// is not valid and keepable commits nothing, so its copies fall to the next stage or stay /// remaining, listed as unplaced when no later stage takes them. /// runs a request; attaches a kept result's sheets to the nest's /// plates on the nest's own thread and returns them. Attaching the parts counts them as /// nested, which is how the next stage asks only for the rest. Each stage may use up to /// sheets. /// public static async Task RunStagedAsync(Nest nest, string engineName, IReadOnlyList firstStock, double minimumSalvageDimension, Func> solve, Func> commit, CancellationToken token = default) { ArgumentNullException.ThrowIfNull(nest); ArgumentNullException.ThrowIfNull(solve); ArgumentNullException.ThrowIfNull(commit); token.ThrowIfCancellationRequested(); var applied = new List(); var committed = new List<(Plate Plate, string StockId)>(); IReadOnlyList unplaced = Array.Empty(); var anyRequest = false; var firstStockNotKept = false; var stages = firstStock is { Count: > 0 } ? new[] { firstStock, null } : new IReadOnlyList[] { null }; foreach (var stock in stages) { var request = CreateRequest(nest, engineName, minimumSalvageDimension, stock, remainingOnly: true); if (request == null) break; anyRequest = true; var result = await solve(request); // Engines that ignore cancellation still have their proposal discarded. token.ThrowIfCancellationRequested(); unplaced = result.Unplaced; if (!IsKeepable(result)) { if (!result.IsValid || !result.CanKeep) { // Every copy this stage asked for is left, not just what its layout missed. unplaced = NotKept(request, result); firstStockNotKept |= stock != null; } continue; } var plates = commit(result); committed.AddRange(StockIds(result, plates)); applied.AddRange(plates); } if (!anyRequest) return new NestRenestResult(false, 0, 0, Array.Empty(), NothingToNestNote); if (applied.Count == 0) return new NestRenestResult(false, 0, 0, unplaced, NotKeptNote) { FirstStockNotKept = firstStockNotKept, }; return Summarize(applied, unplaced) with { Committed = committed, FirstStockNotKept = firstStockNotKept, }; } /// /// The Auto Nest job for every drawing's required quantity of , on /// sheets from its plate defaults and plate-size options; null when nothing has a quantity. /// The nest is not changed, so a caller showing it can solve this off its thread. /// public static NestPipelineRequest CreateRequest(Nest nest, string engineName, double minimumSalvageDimension = 0) => CreateRequest(nest, engineName, minimumSalvageDimension, null, remainingOnly: false); // stock: offered instead of the nest's plate-size options; remainingOnly: copies already // placed are not asked for again. private static NestPipelineRequest CreateRequest(Nest nest, string engineName, double minimumSalvageDimension, IReadOnlyList stock, bool remainingOnly) { ArgumentNullException.ThrowIfNull(nest); int Wanted(Drawing d) => remainingOnly ? d.Quantity.Remaining : d.Quantity.Required; var items = nest.Drawings .Where(d => !d.IsCutOff && Wanted(d) > 0) .Select(d => new NestItem { Drawing = d, Quantity = Wanted(d), Priority = d.Priority, RotationStart = d.Constraints.StartAngle, RotationEnd = d.Constraints.EndAngle, StepAngle = d.Constraints.StepAngle, }) .ToList(); if (items.Count == 0) return null; var options = stock ?? nest.PlateOptions; return new NestPipelineRequest(engineName, items, NestStockBuilder.FromTemplate(nest.PlateDefaults.CreateNew(), options), new NestJobOptions(maxPlates: MaxPlates, salvageRate: options.Count > 0 ? nest.SalvageRate : 0, minimumSalvageDimension: options.Count > 0 ? minimumSalvageDimension : 0) { // The engine has no machine defaults: a plate's saved cutting kerf, if any. CommonLineUsage = nest.CommonLineUsage, CommonLineKerf = nest.Plates.Select(p => p.CuttingParameters?.Kerf ?? 0) .FirstOrDefault(k => double.IsFinite(k) && k > 0), }); } /// /// Keeps only when it is valid, keepable and places something: /// then attaches its sheets to the nest (on the nest's own /// thread) and returns them. Otherwise nothing is committed and the note says so. /// public static NestRenestResult Keep(NestPipelineResult result, Func> commit) { ArgumentNullException.ThrowIfNull(result); ArgumentNullException.ThrowIfNull(commit); if (!IsKeepable(result)) return new NestRenestResult(false, 0, 0, result.Unplaced, NotKeptNote); var applied = commit(result); return Summarize(applied, result.Unplaced) with { Committed = StockIds(result, applied) }; } private static List<(Plate Plate, string StockId)> StockIds(NestPipelineResult result, IReadOnlyList applied) => NestPipelineCommit.PairWithStock(result, applied) .Select(p => (p.Plate, p.Stock.Id)) .ToList(); // The copies the layout missed keep their own reasons; the ones it placed were not kept. private static IReadOnlyList NotKept(NestPipelineRequest request, NestPipelineResult result) { var unplaced = result.Unplaced.ToList(); foreach (var item in request.Items) { var listed = unplaced.Where(u => ReferenceEquals(u.Drawing, item.Drawing)) .Sum(u => u.Count); if (item.Quantity > listed) unplaced.Add(new NestUnplacedPart(item.Drawing.Name, item.Drawing, item.Quantity - listed, NestUnplacedReason.ProposalNotKeepable)); } return unplaced; } private static bool IsKeepable(NestPipelineResult result) => result.IsValid && result.CanKeep && result.Plates.Any(p => p.Parts.Count > 0); private static NestRenestResult Summarize(IReadOnlyList applied, IReadOnlyList unplaced) { var sheets = applied.Sum(p => p.Quantity); var area = applied.Sum(p => p.Area() * p.Quantity); var utilization = area > 0 ? applied.Sum(p => p.Utilization() * p.Area() * p.Quantity) / area : 0; return new NestRenestResult(true, sheets, utilization, unplaced, Note(sheets, utilization, unplaced)); } private static string Note(int sheets, double utilization, IReadOnlyList unplaced) { var note = string.Format(CultureInfo.CurrentCulture, "Nested on {0} sheet{1}, {2:P0} used.", sheets, sheets == 1 ? "" : "s", utilization); if (unplaced.Count == 0) return note; var copies = unplaced.Sum(u => u.Count); return note + $" {copies} cop{(copies == 1 ? "y" : "ies")} not placed, left to nest: " + string.Join("; ", unplaced.Select(u => u.Describe())) + "."; } }