using System.Globalization; using OpenNest.Geometry; namespace OpenNest.Data.MaterialLedger; /// /// The MaterialLedger stock one released nest is offered: the rows Auto Nest's ledger load /// would tick (substitutes included). An empty offer nests on the plate defaults alone, and /// says why the ledger was not used. /// public sealed record ReleaseLedgerOffer(IReadOnlyList Rows, string? NotUsed) { public static ReleaseLedgerOffer None(string reason) => new(Array.Empty(), reason); /// Why codes on other nests were not left out of the rows, or null. public string? OtherNestsUnchecked { get; init; } /// /// The rows as engine stock: finite, under their ledger:{n} ids, no costs. /// public IReadOnlyList Options { get; } = (Rows ?? throw new ArgumentNullException(nameof(Rows))) .Select(row => new PlateOption { Width = row.Size.Width, Length = row.Size.Length, Quantity = row.Quantity, Id = row.Id, }) .ToList(); } /// /// MaterialLedger stock for one Make Nests run. Each nest is offered the free sheets and /// drops of its material, grade and thickness, minus codes on other open server nests (read /// once per run) and the sheets nests earlier in the same run took: those are not on the /// server until the release uploads them. Nothing here throws for an unusable ledger; the /// offer says so. /// public sealed class ReleaseLedgerStock { /// Reads the free stock for a material, grade and thickness in inches. public delegate Task StockReader(string material, string grade, double thicknessIn, CancellationToken cancellationToken); private readonly StockReader _readStock; private readonly Func> _readCodesInUse; // Sheets taken per code: a bundle repeats its one code per sheet. private readonly Dictionary _taken = new(StringComparer.Ordinal); private LedgerCodesInUse? _codesInUse; public ReleaseLedgerStock(StockReader readStock, Func> readCodesInUse) { _readStock = readStock ?? throw new ArgumentNullException(nameof(readStock)); _readCodesInUse = readCodesInUse ?? throw new ArgumentNullException(nameof(readCodesInUse)); } /// The sheets nests of this run have taken so far, per code. public IReadOnlyDictionary Taken => new Dictionary(_taken, StringComparer.Ordinal); /// /// The stock to offer , whose drawings carry their release /// quantities. /// public async Task OfferAsync(Nest nest, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(nest); var material = nest.Material?.Name?.Trim() ?? ""; if (material.Length == 0 || !(nest.Thickness > 0)) return ReleaseLedgerOffer.None("the nest has no material or thickness"); var grade = nest.Material?.Grade?.Trim() ?? ""; var thicknessIn = LedgerStockMapper.ThicknessInches(nest.Thickness, nest.Units); var result = await _readStock(material, grade, thicknessIn, cancellationToken); var described = Describe(nest, material, grade); switch (result) { case LedgerStockResult.Ok ok: var mapping = LedgerStockMapper.Map(ok.Items, nest.Units, PartEnvelopes(nest), ok.Specs); _codesInUse ??= await _readCodesInUse(cancellationToken); if (_codesInUse.IsKnown) mapping = LedgerCodeFilter.Exclude(mapping, _codesInUse.Codes); var rows = LedgerCodeFilter.ExcludeCopies(mapping.Rows, _taken, out var taken); if (rows.Count == 0) return ReleaseLedgerOffer.None(Unusable(described, mapping, taken)); return new ReleaseLedgerOffer(rows, null) { OtherNestsUnchecked = _codesInUse.Unavailable, }; case LedgerStockResult.Unmapped: return ReleaseLedgerOffer.None($"no mapping for {described}"); case LedgerStockResult.Unauthorized: return ReleaseLedgerOffer.None("it rejected the API key (Tools > Options)"); case LedgerStockResult.Unreachable unreachable: return ReleaseLedgerOffer.None($"unreachable: {unreachable.Reason.TrimEnd('.')}"); default: return ReleaseLedgerOffer.None("not set up on this PC (Tools > Options)"); } } /// /// Gives the plates just committed on 's rows their item codes, as /// Auto Nest does, and keeps those sheets from later nests of this run. Call it inside the /// commit's undo step. Returns each assigned code's ledger location, for display. /// /// /// Each plate the commit attached, with the engine stock id it was solved on. /// public IReadOnlyDictionary AssignCodes(Nest nest, ReleaseLedgerOffer offer, IReadOnlyList<(Plate Plate, string StockId)> committed) { ArgumentNullException.ThrowIfNull(nest); ArgumentNullException.ThrowIfNull(offer); ArgumentNullException.ThrowIfNull(committed); var locations = new Dictionary(StringComparer.Ordinal); LedgerStockAssignment.Assign(nest.Plates, committed, offer.Rows); foreach (var (plate, _) in committed) { foreach (var code in plate.StockItemCodes) { _taken[code] = _taken.GetValueOrDefault(code) + 1; var row = offer.Rows.FirstOrDefault(r => r.ItemLocations.ContainsKey(code)); if (row != null) locations[code] = row.ItemLocations[code]; } } return locations; } /// /// The release note for a nest nested on : how many sheets came /// from the ledger, or why it was not used. /// /// The ledger codes gave the nest. /// /// True when Auto Nest could not keep a layout on the offer. /// public static string Note(ReleaseLedgerOffer offer, int sheets, bool layoutNotKept) { ArgumentNullException.ThrowIfNull(offer); if (offer.NotUsed != null) return $"MaterialLedger not used: {offer.NotUsed}."; var note = layoutNotKept ? "MaterialLedger not used: Auto Nest could not keep a layout on its stock." : sheets == 0 ? "No MaterialLedger stock used: Auto Nest placed nothing on it." : string.Format(CultureInfo.CurrentCulture, "{0} sheet{1} from MaterialLedger stock.", sheets, sheets == 1 ? "" : "s"); return offer.OtherNestsUnchecked == null ? note : $"{note} Codes on other nests were not checked: {offer.OtherNestsUnchecked}."; } // Why no row is left, in the counts Auto Nest's ledger summary uses. private static string Unusable(string described, LedgerStockMapping mapping, int taken) { var reasons = new List(); if (mapping.OnOtherNests > 0) reasons.Add($"{mapping.OnOtherNests} on other nests"); if (taken > 0) reasons.Add($"{taken} taken by earlier nests of this release"); if (mapping.SkippedTooSmall > 0) reasons.Add($"{mapping.SkippedTooSmall} too small for any part"); if (mapping.SkippedUnknownShape > 0) reasons.Add($"{mapping.SkippedUnknownShape} of unknown shape"); return reasons.Count == 0 ? $"no free stock of {described}" : $"no usable stock of {described} ({string.Join(", ", reasons)})"; } private static List PartEnvelopes(Nest nest) { var envelopes = new List(); var drawings = nest.Drawings .Where(d => !d.IsCutOff && d.Quantity.Required > 0 && d.Program != null); foreach (var drawing in drawings) { var box = drawing.Program.BoundingBox(); if (double.IsFinite(box.Width) && double.IsFinite(box.Length) && box.Width > 0 && box.Length > 0) envelopes.Add(new Size(box.Width, box.Length)); } return envelopes; } private static string Describe(Nest nest, string material, string grade) { var unit = nest.Units == Units.Millimeters ? "mm" : "in"; return FormattableString.Invariant( $"{material}{(grade.Length == 0 ? "" : " " + grade)} {nest.Thickness:G} {unit}"); } }