mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-04 03:31:36 -05:00
fix(release): keep ledger codes in the undo step and share bundles per sheet
Review fixes for ledger-first Make Nests: - The release renest (offer, staged run, codes, note) moves into OpenNest.Desktop.ReleaseRenest, shared by the headless panel, the live main window and the tests. Codes are given inside each commit, so the live tab's Auto Nest undo step records them; before, undo and redo dropped every ledger code. - A bundle repeats one code per sheet: a later nest of the run lost the whole bundle once an earlier nest took one sheet. Taken sheets are now counted per code (LedgerCodeFilter.ExcludeCopies). - A stage whose layout is not kept lists every copy it asked for as not placed (the proposal cannot be kept), and a rejected ledger layout says "MaterialLedger not used: Auto Nest could not keep a layout on its stock" instead of a bare "No MaterialLedger stock used". - "no free stock" is said only when the ledger had none; otherwise the note counts what was left out (on other nests, taken by earlier nests of this release, too small, unknown shape). - The ledger is not read for a nest with nothing to nest, the live read runs under the tab's stop token, a reason's own full stop is not doubled, and the sheet count is the codes given.
This commit is contained in:
1 parent
31ee77914f
commit
c5b6e680cb
10 files changed
+580
-251
No files matched your search
@@ -20,13 +20,41 @@ public static class LedgerCodeFilter
|
||||
ArgumentNullException.ThrowIfNull(rows);
|
||||
ArgumentNullException.ThrowIfNull(codesInUse);
|
||||
var exclude = codesInUse as IReadOnlySet<string> ?? codesInUse.ToHashSet(StringComparer.Ordinal);
|
||||
return Rebuild(rows, exclude.Contains, out removed);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <paramref name="rows"/> with only <paramref name="copies"/> of each code taken out, in
|
||||
/// row and ledger order: a bundle whose one code repeats per sheet loses as many sheets
|
||||
/// as were taken, not the whole bundle. Otherwise as
|
||||
/// <see cref="Exclude(IEnumerable{LedgerStockRow}, IEnumerable{string}, out int)"/>.
|
||||
/// </summary>
|
||||
public static IReadOnlyList<LedgerStockRow> ExcludeCopies(IEnumerable<LedgerStockRow> rows,
|
||||
IReadOnlyDictionary<string, int> copies, out int removed)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(rows);
|
||||
ArgumentNullException.ThrowIfNull(copies);
|
||||
var left = new Dictionary<string, int>(copies, StringComparer.Ordinal);
|
||||
return Rebuild(rows, code =>
|
||||
{
|
||||
if (!left.TryGetValue(code, out var count) || count <= 0)
|
||||
return false;
|
||||
left[code] = count - 1;
|
||||
return true;
|
||||
}, out removed);
|
||||
}
|
||||
|
||||
// takeOut is asked once per copy, in row order and the ledger order within a row.
|
||||
private static IReadOnlyList<LedgerStockRow> Rebuild(IEnumerable<LedgerStockRow> rows,
|
||||
Func<string, bool> takeOut, out int removed)
|
||||
{
|
||||
removed = 0;
|
||||
var kept = new List<LedgerStockRow>();
|
||||
foreach (var row in rows)
|
||||
{
|
||||
if (row == null)
|
||||
continue;
|
||||
var codes = row.ItemCodes.Where(code => !exclude.Contains(code)).ToList();
|
||||
var codes = row.ItemCodes.Where(code => !takeOut(code)).ToList();
|
||||
var dropped = row.ItemCodes.Count - codes.Count;
|
||||
if (dropped == 0)
|
||||
{
|
||||
@@ -37,8 +65,9 @@ public static class LedgerCodeFilter
|
||||
if (codes.Count == 0)
|
||||
continue;
|
||||
|
||||
var left = codes.ToHashSet(StringComparer.Ordinal);
|
||||
var itemLocations = row.ItemLocations
|
||||
.Where(pair => !exclude.Contains(pair.Key))
|
||||
.Where(pair => left.Contains(pair.Key) || !row.ItemCodes.Contains(pair.Key))
|
||||
.ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.Ordinal);
|
||||
// A location stays listed while an item left in the row is there, or when the row
|
||||
// never said which item it held.
|
||||
|
||||
@@ -19,7 +19,7 @@ public sealed record ReleaseLedgerOffer(IReadOnlyList<LedgerStockRow> Rows, stri
|
||||
/// <summary>
|
||||
/// The rows as engine stock: finite, under their <c>ledger:{n}</c> ids, no costs.
|
||||
/// </summary>
|
||||
public IReadOnlyList<PlateOption> Options => Rows
|
||||
public IReadOnlyList<PlateOption> Options { get; } = Rows
|
||||
.Select(row => new PlateOption
|
||||
{
|
||||
Width = row.Size.Width,
|
||||
@@ -33,9 +33,9 @@ public sealed record ReleaseLedgerOffer(IReadOnlyList<LedgerStockRow> Rows, stri
|
||||
/// <summary>
|
||||
/// 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 codes 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.
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public sealed class ReleaseLedgerStock
|
||||
{
|
||||
@@ -45,7 +45,8 @@ public sealed class ReleaseLedgerStock
|
||||
|
||||
private readonly StockReader _readStock;
|
||||
private readonly Func<CancellationToken, Task<LedgerCodesInUse>> _readCodesInUse;
|
||||
private readonly HashSet<string> _taken = new(StringComparer.Ordinal);
|
||||
// Sheets taken per code: a bundle repeats its one code per sheet.
|
||||
private readonly Dictionary<string, int> _taken = new(StringComparer.Ordinal);
|
||||
private LedgerCodesInUse? _codesInUse;
|
||||
|
||||
public ReleaseLedgerStock(StockReader readStock,
|
||||
@@ -56,8 +57,9 @@ public sealed class ReleaseLedgerStock
|
||||
?? throw new ArgumentNullException(nameof(readCodesInUse));
|
||||
}
|
||||
|
||||
/// <summary>The codes nests of this run have taken so far.</summary>
|
||||
public IReadOnlyCollection<string> Taken => _taken;
|
||||
/// <summary>The sheets nests of this run have taken so far, per code.</summary>
|
||||
public IReadOnlyDictionary<string, int> Taken =>
|
||||
new Dictionary<string, int>(_taken, StringComparer.Ordinal);
|
||||
|
||||
/// <summary>
|
||||
/// The stock to offer <paramref name="nest"/>, whose drawings carry their release
|
||||
@@ -73,6 +75,7 @@ public sealed class ReleaseLedgerStock
|
||||
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:
|
||||
@@ -81,60 +84,96 @@ public sealed class ReleaseLedgerStock
|
||||
_codesInUse ??= await _readCodesInUse(cancellationToken);
|
||||
if (_codesInUse.IsKnown)
|
||||
mapping = LedgerCodeFilter.Exclude(mapping, _codesInUse.Codes);
|
||||
var rows = LedgerCodeFilter.Exclude(mapping.Rows, _taken, out _);
|
||||
var rows = LedgerCodeFilter.ExcludeCopies(mapping.Rows, _taken, out var taken);
|
||||
if (rows.Count == 0)
|
||||
return ReleaseLedgerOffer.None(
|
||||
$"no free stock of {Describe(nest, material, grade)}");
|
||||
return ReleaseLedgerOffer.None(Unusable(described, mapping, taken));
|
||||
return new ReleaseLedgerOffer(rows, null)
|
||||
{
|
||||
OtherNestsUnchecked = _codesInUse.Unavailable,
|
||||
};
|
||||
case LedgerStockResult.Unmapped:
|
||||
return ReleaseLedgerOffer.None($"no mapping for {Describe(nest, material, grade)}");
|
||||
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}");
|
||||
return ReleaseLedgerOffer.None($"unreachable: {unreachable.Reason.TrimEnd('.')}");
|
||||
default:
|
||||
return ReleaseLedgerOffer.None("not set up on this PC (Tools > Options)");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gives the plates the release committed on <paramref name="offer"/>'s rows their item
|
||||
/// codes, as Auto Nest does, keeps those codes from later nests of this run, and returns
|
||||
/// the note for the release: how many sheets came from the ledger, or why it was not used.
|
||||
/// Gives the plates just committed on <paramref name="offer"/>'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.
|
||||
/// </summary>
|
||||
/// <param name="committed">
|
||||
/// Each committed plate with the engine stock id it was solved on.
|
||||
/// Each plate the commit attached, with the engine stock id it was solved on.
|
||||
/// </param>
|
||||
public string Assign(Nest nest, ReleaseLedgerOffer offer,
|
||||
public IReadOnlyDictionary<string, string> AssignCodes(Nest nest, ReleaseLedgerOffer offer,
|
||||
IReadOnlyList<(Plate Plate, string StockId)> committed)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(nest);
|
||||
ArgumentNullException.ThrowIfNull(offer);
|
||||
ArgumentNullException.ThrowIfNull(committed);
|
||||
var locations = new Dictionary<string, string>(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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The release note for <paramref name="nest"/> nested on <paramref name="offer"/>: how
|
||||
/// many sheets came from the ledger, or why it was not used.
|
||||
/// </summary>
|
||||
/// <param name="layoutNotKept">
|
||||
/// True when Auto Nest could not keep a layout on the offer.
|
||||
/// </param>
|
||||
public static string Note(ReleaseLedgerOffer offer, Nest nest, bool layoutNotKept)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(offer);
|
||||
ArgumentNullException.ThrowIfNull(nest);
|
||||
if (offer.NotUsed != null)
|
||||
return $"MaterialLedger not used: {offer.NotUsed}.";
|
||||
|
||||
LedgerStockAssignment.Assign(nest.Plates, committed, offer.Rows);
|
||||
var sheets = 0;
|
||||
foreach (var (plate, stockId) in committed)
|
||||
{
|
||||
_taken.UnionWith(plate.StockItemCodes);
|
||||
if (LedgerStockMapper.IsLedgerId(stockId))
|
||||
sheets += plate.Quantity;
|
||||
}
|
||||
|
||||
var note = sheets == 0
|
||||
? "No MaterialLedger stock used."
|
||||
: string.Format(CultureInfo.CurrentCulture, "{0} sheet{1} from MaterialLedger stock.",
|
||||
sheets, sheets == 1 ? "" : "s");
|
||||
var sheets = nest.Plates.Sum(p => p.StockItemCodes.Count);
|
||||
var note = layoutNotKept
|
||||
? "MaterialLedger not used: Auto Nest could not keep a layout on its stock."
|
||||
: sheets == 0
|
||||
? "No MaterialLedger stock used: no part fits 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<string>();
|
||||
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<Size> PartEnvelopes(Nest nest)
|
||||
{
|
||||
var envelopes = new List<Size>();
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
using OpenNest.Data;
|
||||
using OpenNest.Data.MaterialLedger;
|
||||
using OpenNest.Engine;
|
||||
using OpenNest.Engine.Jobs;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Shapes;
|
||||
|
||||
namespace OpenNest.Desktop.Tests;
|
||||
|
||||
public class ReleaseRenestTests
|
||||
{
|
||||
private const string Material = "Synthetic Steel";
|
||||
|
||||
private static Nest NestOf(double partSize, int quantity, string grade = "SYN-1")
|
||||
{
|
||||
var nest = new Nest("N26-0001")
|
||||
{
|
||||
Material = new Material(Material, grade),
|
||||
Thickness = 0.25,
|
||||
};
|
||||
nest.PlateDefaults.Size = new Size(48, 96);
|
||||
var drawing = new RectangleShape { Name = "SYN-A", Length = partSize, Width = partSize }
|
||||
.GetDrawing();
|
||||
drawing.Quantity.Required = quantity;
|
||||
nest.Drawings.Add(drawing);
|
||||
return nest;
|
||||
}
|
||||
|
||||
private static LedgerStockItem Sheet(string code, decimal width, decimal length) =>
|
||||
new(code, 1, LedgerItemKinds.FullSheet, width, length, 1, LedgerItemShapes.Rectangular,
|
||||
"R1", "Rack 1", null, null, DateTime.UnixEpoch);
|
||||
|
||||
private static LedgerStockItem Bundle(string code, decimal width, decimal length,
|
||||
int quantity) =>
|
||||
new(code, 1, LedgerItemKinds.FullSheetBundle, width, length, quantity,
|
||||
LedgerItemShapes.Rectangular, "B3", "Bay 3", null, null, DateTime.UnixEpoch);
|
||||
|
||||
private static LedgerStockResult Stock(params LedgerStockItem[] items) =>
|
||||
new LedgerStockResult.Ok(Array.Empty<LedgerStockSpec>(), items);
|
||||
|
||||
private sealed class FakeLedger
|
||||
{
|
||||
public LedgerStockResult Stock { get; init; } = Stock();
|
||||
public LedgerCodesInUse InUse { get; init; } =
|
||||
LedgerCodesInUse.Known(Array.Empty<NestStockCodeUse>());
|
||||
public List<(string Material, string Grade, double Thickness)> StockReads { get; } = new();
|
||||
public int InUseReads { get; private set; }
|
||||
|
||||
public ReleaseLedgerStock Create() => new(
|
||||
(material, grade, thickness, _) =>
|
||||
{
|
||||
StockReads.Add((material, grade, thickness));
|
||||
return Task.FromResult(Stock);
|
||||
},
|
||||
_ =>
|
||||
{
|
||||
InUseReads++;
|
||||
return Task.FromResult(InUse);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rectangles, except that a sheet <c>overlaps</c> picks gets every copy at one spot.
|
||||
/// </summary>
|
||||
private sealed class OverlapOn(Func<string, bool> overlaps) : INestingEngine
|
||||
{
|
||||
private readonly INestingEngine _inner = NestingEngineRegistry.Create("Rectangles");
|
||||
|
||||
public NestJobResult Solve(NestJob job, IProgress<NestJobProgress> progress = null,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
if (!overlaps(job.Plates[0].Id))
|
||||
return _inner.Solve(job, progress, token);
|
||||
var builder = new NestJobResultBuilder(job);
|
||||
builder.AddSheet(job.Plates[0], Enumerable.Range(0, job.Parts[0].Quantity)
|
||||
.Select(_ => (job.Parts[0].Id, 5.0, 5.0, 0.0)).ToArray());
|
||||
return builder.Build(NestJobStopReason.Completed);
|
||||
}
|
||||
}
|
||||
|
||||
private static Task<ReleaseRenestResult> Release(ReleaseLedgerStock ledger, Nest nest,
|
||||
INestingEngine engine = null) =>
|
||||
ReleaseRenest.RunHeadlessAsync(nest, ledger,
|
||||
engine ?? NestingEngineRegistry.Create("Rectangles"), "Rectangles", 0,
|
||||
CancellationToken.None);
|
||||
|
||||
[Fact]
|
||||
public async Task LedgerStockIsUsedFirst_EvenWhenTheDefaultSheetWouldBeSmaller()
|
||||
{
|
||||
// One 60x120 ledger sheet holds both parts; a single pass scored by area would take
|
||||
// the smaller 48x96 Nest Defaults sheet instead.
|
||||
var ledger = new FakeLedger { Stock = Stock(Sheet("S-1", 60, 120)) };
|
||||
var nest = NestOf(20, 2);
|
||||
|
||||
var result = await Release(ledger.Create(), nest);
|
||||
|
||||
Assert.True(result.Nested, result.Note);
|
||||
var plate = Assert.Single(nest.Plates);
|
||||
Assert.Equal(new Size(60, 120), plate.Size);
|
||||
Assert.Equal(["S-1"], plate.StockItemCodes);
|
||||
Assert.Equal(0, nest.Drawings.First().Quantity.Remaining);
|
||||
Assert.EndsWith(" 1 sheet from MaterialLedger stock.", result.Note);
|
||||
Assert.Equal("R1", result.LedgerLocations["S-1"]);
|
||||
Assert.Equal((Material, "SYN-1", 0.25), Assert.Single(ledger.StockReads));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CopiesTheLedgerCannotHold_TopUpOnTheNestDefaultsSheet_WithoutACode()
|
||||
{
|
||||
// Two free 40x40 sheets each hold one 30x30 part; the third copy needs a default sheet.
|
||||
var ledger = new FakeLedger { Stock = Stock(Sheet("S-1", 40, 40), Sheet("S-2", 40, 40)) };
|
||||
var nest = NestOf(30, 3);
|
||||
|
||||
var result = await Release(ledger.Create(), nest);
|
||||
|
||||
Assert.True(result.Nested, result.Note);
|
||||
Assert.Equal(0, nest.Drawings.First().Quantity.Remaining);
|
||||
var coded = nest.Plates.Where(p => p.StockItemCodes.Count > 0).ToList();
|
||||
Assert.Equal(["S-1", "S-2"], coded.SelectMany(p => p.StockItemCodes).Order());
|
||||
Assert.All(coded, p => Assert.Equal(new Size(40, 40), p.Size));
|
||||
var topUp = Assert.Single(nest.Plates.Where(p => p.StockItemCodes.Count == 0));
|
||||
Assert.Equal(new Size(48, 96), topUp.Size);
|
||||
Assert.StartsWith("Nested on 3 sheets, ", result.Note);
|
||||
Assert.EndsWith(" 2 sheets from MaterialLedger stock.", result.Note);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ALaterNestOfTheSameRun_DoesNotGetASheetAnEarlierNestTook()
|
||||
{
|
||||
// The release uploads every nest only after all are nested, so the server cannot
|
||||
// report the first nest's codes yet; a substitute row can list the same sheet twice.
|
||||
var ledger = new FakeLedger { Stock = Stock(Sheet("S-1", 60, 120)) };
|
||||
var run = ledger.Create();
|
||||
await Release(run, NestOf(20, 2));
|
||||
|
||||
var second = NestOf(20, 2, grade: "SYN-2");
|
||||
var result = await Release(run, second);
|
||||
|
||||
Assert.True(result.Nested, result.Note);
|
||||
var plate = Assert.Single(second.Plates);
|
||||
Assert.Empty(plate.StockItemCodes);
|
||||
Assert.Equal(new Size(48, 96), plate.Size);
|
||||
Assert.EndsWith(" MaterialLedger not used: no usable stock of Synthetic Steel SYN-2 0.25 in"
|
||||
+ " (1 taken by earlier nests of this release).", result.Note);
|
||||
Assert.Equal(1, run.Taken["S-1"]);
|
||||
Assert.Equal(1, ledger.InUseReads);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ABundleIsSharedSheetBySheet_AcrossNestsOfTheRun()
|
||||
{
|
||||
var ledger = new FakeLedger { Stock = Stock(Bundle("B-1", 60, 120, 10)) };
|
||||
var run = ledger.Create();
|
||||
await Release(run, NestOf(20, 2));
|
||||
|
||||
var offer = await run.OfferAsync(NestOf(20, 2, grade: "SYN-2"), CancellationToken.None);
|
||||
|
||||
Assert.Equal(9, Assert.Single(offer.Rows).Quantity);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CodesOnOtherServerNests_AreLeftOut_AndNamedWhenNothingIsLeft()
|
||||
{
|
||||
var inUse = LedgerCodesInUse.Known(
|
||||
[new NestStockCodeUse { ItemCode = "S-1", NestName = "N26-0099" }]);
|
||||
var partly = new FakeLedger
|
||||
{
|
||||
Stock = Stock(Sheet("S-1", 60, 120), Sheet("S-2", 60, 120)),
|
||||
InUse = inUse,
|
||||
};
|
||||
var all = new FakeLedger { Stock = Stock(Sheet("S-1", 60, 120)), InUse = inUse };
|
||||
|
||||
var offer = await partly.Create().OfferAsync(NestOf(20, 2), CancellationToken.None);
|
||||
var none = await all.Create().OfferAsync(NestOf(20, 2), CancellationToken.None);
|
||||
|
||||
Assert.Null(offer.NotUsed);
|
||||
Assert.Equal(["S-2"], Assert.Single(offer.Rows).ItemCodes);
|
||||
Assert.Equal("no usable stock of Synthetic Steel SYN-1 0.25 in (1 on other nests)",
|
||||
none.NotUsed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NoStockAtAll_IsCalledNoFreeStock()
|
||||
{
|
||||
var result = await Release(new FakeLedger().Create(), NestOf(20, 2));
|
||||
|
||||
Assert.EndsWith(" MaterialLedger not used: no free stock of Synthetic Steel SYN-1 0.25 in.",
|
||||
result.Note);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("unreachable", "unreachable: No answer within 10 s")]
|
||||
[InlineData("unauthorized", "it rejected the API key (Tools > Options)")]
|
||||
[InlineData("unmapped", "no mapping for Synthetic Steel SYN-1 0.25 in")]
|
||||
[InlineData("not configured", "not set up on this PC (Tools > Options)")]
|
||||
public async Task AnUnusableLedger_NestsOnTheNestDefaultsSheet_AndSaysWhy(string state,
|
||||
string why)
|
||||
{
|
||||
LedgerStockResult stock = state switch
|
||||
{
|
||||
"unreachable" => new LedgerStockResult.Unreachable("No answer within 10 s."),
|
||||
"unauthorized" => new LedgerStockResult.Unauthorized(),
|
||||
"unmapped" => new LedgerStockResult.Unmapped(),
|
||||
_ => new LedgerStockResult.NotConfigured(),
|
||||
};
|
||||
var ledger = new FakeLedger { Stock = stock };
|
||||
var nest = NestOf(20, 2);
|
||||
|
||||
var result = await Release(ledger.Create(), nest);
|
||||
|
||||
Assert.True(result.Nested, result.Note);
|
||||
var plate = Assert.Single(nest.Plates);
|
||||
Assert.Equal(new Size(48, 96), plate.Size);
|
||||
Assert.Empty(plate.StockItemCodes);
|
||||
Assert.EndsWith($" MaterialLedger not used: {why}.", result.Note);
|
||||
Assert.Equal(0, ledger.InUseReads);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UncheckedOtherNests_AreNamedInTheNote()
|
||||
{
|
||||
var ledger = new FakeLedger
|
||||
{
|
||||
Stock = Stock(Sheet("S-1", 60, 120)),
|
||||
InUse = LedgerCodesInUse.Unknown("the nest server did not answer"),
|
||||
};
|
||||
|
||||
var result = await Release(ledger.Create(), NestOf(20, 2));
|
||||
|
||||
Assert.EndsWith(" 1 sheet from MaterialLedger stock. Codes on other nests were not "
|
||||
+ "checked: the nest server did not answer.", result.Note);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NothingToNest_DoesNotReadTheLedger()
|
||||
{
|
||||
var ledger = new FakeLedger { Stock = Stock(Sheet("S-1", 60, 120)) };
|
||||
|
||||
var result = await Release(ledger.Create(), NestOf(20, 0));
|
||||
|
||||
Assert.Equal((false, NestRenest.NothingToNestNote), (result.Nested, result.Note));
|
||||
Assert.Empty(ledger.StockReads);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ARejectedLedgerLayout_FallsToTheNestDefaults_AndSaysTheLedgerWasNotUsed()
|
||||
{
|
||||
var ledger = new FakeLedger { Stock = Stock(Sheet("S-1", 60, 120)) };
|
||||
var nest = NestOf(20, 2);
|
||||
|
||||
var result = await Release(ledger.Create(), nest,
|
||||
new OverlapOn(LedgerStockMapper.IsLedgerId));
|
||||
|
||||
Assert.True(result.Nested, result.Note);
|
||||
Assert.Equal(new Size(48, 96), Assert.Single(nest.Plates).Size);
|
||||
Assert.EndsWith(" MaterialLedger not used: Auto Nest could not keep a layout on its stock.",
|
||||
result.Note);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ARejectedDefaultsLayout_ListsEveryCopyItAskedFor()
|
||||
{
|
||||
// One 40x40 ledger sheet holds one of three copies; the defaults layout never validates.
|
||||
var ledger = new FakeLedger { Stock = Stock(Sheet("S-1", 40, 40)) };
|
||||
var nest = NestOf(30, 3);
|
||||
|
||||
var result = await Release(ledger.Create(), nest,
|
||||
new OverlapOn(id => !LedgerStockMapper.IsLedgerId(id)));
|
||||
|
||||
Assert.True(result.Nested, result.Note);
|
||||
Assert.Equal(2, nest.Drawings.First().Quantity.Remaining);
|
||||
Assert.Contains(" 2 copies not placed, left to nest: "
|
||||
+ "SYN-A: 2 not placed (the proposal cannot be kept).", result.Note);
|
||||
Assert.EndsWith(" 1 sheet from MaterialLedger stock.", result.Note);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CodesAreGivenInsideTheCommit_BeforeItsUndoStepCloses()
|
||||
{
|
||||
var ledger = new FakeLedger { Stock = Stock(Sheet("S-1", 60, 120)) }.Create();
|
||||
var nest = NestOf(20, 2);
|
||||
var codesAtClose = new List<string>();
|
||||
|
||||
await ReleaseRenest.RunAsync(nest, ledger, "Rectangles", 0,
|
||||
request => Task.FromResult(NestPipeline.Run(NestingEngineRegistry.Create("Rectangles"),
|
||||
"Rectangles", request, null, CancellationToken.None)),
|
||||
(result, finish) =>
|
||||
{
|
||||
var plates = NestPipelineCommit.ApplyToPlates(result, nest.CreatePlate);
|
||||
finish(plates);
|
||||
// Where the live tab completes its undo step.
|
||||
codesAtClose.AddRange(plates.SelectMany(p => p.StockItemCodes));
|
||||
return plates;
|
||||
},
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.Equal(["S-1"], codesAtClose);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
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> with those plates before its undo step closes; that gives them their
|
||||
/// ledger codes.
|
||||
/// </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 renested = await NestRenest.RunStagedAsync(nest, engineName, offer?.Options,
|
||||
minimumSalvageDimension, solve,
|
||||
result => commit(result, plates =>
|
||||
{
|
||||
if (offer == null)
|
||||
return;
|
||||
var stock = NestPipelineCommit.PairWithStock(result, plates)
|
||||
.Select(p => (p.Plate, p.Stock.Id))
|
||||
.ToList();
|
||||
foreach (var (code, location) in ledger.AssignCodes(nest, offer, stock))
|
||||
locations[code] = location;
|
||||
}),
|
||||
token);
|
||||
|
||||
var ledgerNote = offer == null
|
||||
? null
|
||||
: ReleaseLedgerStock.Note(offer, nest, 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)));
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,12 @@ public sealed record NestRenestResult(
|
||||
/// </summary>
|
||||
public IReadOnlyList<(Plate Plate, string StockId)> Committed { get; init; } =
|
||||
Array.Empty<(Plate Plate, string StockId)>();
|
||||
|
||||
/// <summary>
|
||||
/// True when a staged run's first stock got a layout that was not valid or not keepable,
|
||||
/// so none of it was used (<see cref="NestRenest.RunStagedAsync"/>).
|
||||
/// </summary>
|
||||
public bool FirstStockNotKept { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -57,16 +63,7 @@ public static class NestRenest
|
||||
/// </param>
|
||||
public static NestRenestResult Run(Nest nest, INestingEngine engine, string engineName,
|
||||
double minimumSalvageDimension = 0, IProgress<NestJobProgress> progress = null,
|
||||
CancellationToken token = default) =>
|
||||
Run(nest, engine, engineName, null, minimumSalvageDimension, progress, token);
|
||||
|
||||
/// <param name="firstStock">
|
||||
/// Sheets offered before the plate defaults (see <see cref="RunStagedAsync"/>); null or
|
||||
/// empty nests on the plate defaults alone.
|
||||
/// </param>
|
||||
public static NestRenestResult Run(Nest nest, INestingEngine engine, string engineName,
|
||||
IReadOnlyList<PlateOption> firstStock, double minimumSalvageDimension = 0,
|
||||
IProgress<NestJobProgress> progress = null, CancellationToken token = default)
|
||||
CancellationToken token = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(nest);
|
||||
ArgumentNullException.ThrowIfNull(engine);
|
||||
@@ -75,7 +72,7 @@ public static class NestRenest
|
||||
nest.Plates.Clear();
|
||||
nest.UpdateDrawingQuantities();
|
||||
// Solved synchronously: the returned task has already completed.
|
||||
return RunStagedAsync(nest, engineName, firstStock, minimumSalvageDimension,
|
||||
return RunStagedAsync(nest, engineName, null, minimumSalvageDimension,
|
||||
request => Task.FromResult(
|
||||
NestPipeline.Run(engine, engineName, request, progress, token)),
|
||||
kept => NestPipelineCommit.ApplyToPlates(kept, nest.CreatePlate, token: token),
|
||||
@@ -89,8 +86,11 @@ public static class NestRenest
|
||||
/// (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. <paramref name="solve"/> runs a request; <paramref name="commit"/> attaches a
|
||||
/// kept result's sheets to the nest on the nest's own thread and returns them.
|
||||
/// remaining, listed as unplaced when no later stage takes them. <paramref name="solve"/>
|
||||
/// runs a request; <paramref name="commit"/> 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
|
||||
/// <see cref="MaxPlates"/> sheets.
|
||||
/// </summary>
|
||||
public static async Task<NestRenestResult> RunStagedAsync(Nest nest, string engineName,
|
||||
IReadOnlyList<PlateOption> firstStock, double minimumSalvageDimension,
|
||||
@@ -106,6 +106,7 @@ public static class NestRenest
|
||||
var committed = new List<(Plate Plate, string StockId)>();
|
||||
IReadOnlyList<NestUnplacedPart> unplaced = Array.Empty<NestUnplacedPart>();
|
||||
var anyRequest = false;
|
||||
var firstStockNotKept = false;
|
||||
var stages = firstStock is { Count: > 0 }
|
||||
? new[] { firstStock, null }
|
||||
: new IReadOnlyList<PlateOption>[] { null };
|
||||
@@ -121,8 +122,15 @@ public static class NestRenest
|
||||
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);
|
||||
firstStockNotKept |= stock != null;
|
||||
}
|
||||
continue;
|
||||
// Attaching parts counts them as nested, so the next stage asks only for the rest.
|
||||
}
|
||||
var plates = commit(result);
|
||||
committed.AddRange(StockIds(result, plates));
|
||||
applied.AddRange(plates);
|
||||
@@ -132,8 +140,15 @@ public static class NestRenest
|
||||
return new NestRenestResult(false, 0, 0, Array.Empty<NestUnplacedPart>(),
|
||||
NothingToNestNote);
|
||||
if (applied.Count == 0)
|
||||
return new NestRenestResult(false, 0, 0, unplaced, NotKeptNote);
|
||||
return Summarize(applied, unplaced) with { Committed = committed };
|
||||
return new NestRenestResult(false, 0, 0, unplaced, NotKeptNote)
|
||||
{
|
||||
FirstStockNotKept = firstStockNotKept,
|
||||
};
|
||||
return Summarize(applied, unplaced) with
|
||||
{
|
||||
Committed = committed,
|
||||
FirstStockNotKept = firstStockNotKept,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -197,6 +212,12 @@ public static class NestRenest
|
||||
.Select(p => (p.Plate, p.Stock.Id))
|
||||
.ToList();
|
||||
|
||||
private static IReadOnlyList<NestUnplacedPart> NotKept(NestPipelineRequest request) =>
|
||||
request.Items
|
||||
.Select(item => new NestUnplacedPart(item.Drawing.Name, item.Drawing, item.Quantity,
|
||||
NestUnplacedReason.ProposalNotKeepable))
|
||||
.ToList();
|
||||
|
||||
private static bool IsKeepable(NestPipelineResult result) =>
|
||||
result.IsValid && result.CanKeep && result.Plates.Any(p => p.Parts.Count > 0);
|
||||
|
||||
|
||||
@@ -142,6 +142,23 @@ public class LedgerCodeFilterTests
|
||||
internal static NestStockCodeUse Use(string code, string nest) =>
|
||||
new() { ItemCode = code, NestId = Guid.NewGuid(), NestName = nest, Status = NestStatus.Quote };
|
||||
|
||||
[Fact]
|
||||
public void ExcludeCopies_TakesOnlyTheCopiesTaken_InLedgerOrder()
|
||||
{
|
||||
var bundle = Row("ledger:1", ["B-1", "B-1", "B-1"], ("B-1", "R1"));
|
||||
var drops = Row("ledger:2", ["D-1", "D-2"], ("D-1", "R1"), ("D-2", "R2"));
|
||||
|
||||
var taken = new Dictionary<string, int> { ["B-1"] = 2, ["D-2"] = 1, ["UNRELATED"] = 1 };
|
||||
|
||||
var rows = LedgerCodeFilter.ExcludeCopies([bundle, drops], taken, out var removed);
|
||||
|
||||
Assert.Equal(3, removed);
|
||||
Assert.Equal((1, "B-1"), (rows[0].Quantity, Assert.Single(rows[0].ItemCodes)));
|
||||
Assert.Equal("R1", rows[0].ItemLocations["B-1"]);
|
||||
Assert.Equal(["D-1"], rows[1].ItemCodes);
|
||||
Assert.Equal(["R1"], rows[1].Locations);
|
||||
}
|
||||
|
||||
private static LedgerStockRow Row(string id, string[] codes, params (string Code, string Location)[] locations) =>
|
||||
new(id, new Size(48, 96), codes.Length, IsDrop: codes.Distinct().Count() > 1, IsIrregular: false, codes,
|
||||
locations.Select(l => l.Location).Distinct().ToList())
|
||||
|
||||
@@ -1,173 +0,0 @@
|
||||
using OpenNest.Data;
|
||||
using OpenNest.Data.MaterialLedger;
|
||||
using OpenNest.Engine.Jobs;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Shapes;
|
||||
|
||||
namespace OpenNest.Tests.Data;
|
||||
|
||||
public class ReleaseLedgerStockTests
|
||||
{
|
||||
private const string Material = "Synthetic Steel";
|
||||
|
||||
private static Nest NestOf(double partSize, int quantity, string grade = "SYN-1")
|
||||
{
|
||||
var nest = new Nest("N26-0001")
|
||||
{
|
||||
Material = new Material(Material, grade),
|
||||
Thickness = 0.25,
|
||||
};
|
||||
nest.PlateDefaults.Size = new Size(48, 96);
|
||||
var drawing = new RectangleShape { Name = "SYN-A", Length = partSize, Width = partSize }
|
||||
.GetDrawing();
|
||||
drawing.Quantity.Required = quantity;
|
||||
nest.Drawings.Add(drawing);
|
||||
return nest;
|
||||
}
|
||||
|
||||
private static LedgerStockItem Sheet(string code, decimal width, decimal length) =>
|
||||
new(code, 1, LedgerItemKinds.FullSheet, width, length, 1, LedgerItemShapes.Rectangular,
|
||||
"R1", "Rack 1", null, null, DateTime.UnixEpoch);
|
||||
|
||||
private static LedgerStockResult Stock(params LedgerStockItem[] items) =>
|
||||
new LedgerStockResult.Ok(Array.Empty<LedgerStockSpec>(), items);
|
||||
|
||||
private sealed class FakeLedger
|
||||
{
|
||||
public LedgerStockResult Stock { get; init; } = Stock();
|
||||
public LedgerCodesInUse InUse { get; init; } =
|
||||
LedgerCodesInUse.Known(Array.Empty<NestStockCodeUse>());
|
||||
public List<(string Material, string Grade, double Thickness)> StockReads { get; } = new();
|
||||
public int InUseReads { get; private set; }
|
||||
|
||||
public ReleaseLedgerStock Create() => new(
|
||||
(material, grade, thickness, _) =>
|
||||
{
|
||||
StockReads.Add((material, grade, thickness));
|
||||
return Task.FromResult(Stock);
|
||||
},
|
||||
_ =>
|
||||
{
|
||||
InUseReads++;
|
||||
return Task.FromResult(InUse);
|
||||
});
|
||||
}
|
||||
|
||||
private static async Task<(NestRenestResult Renest, string Note)> Release(
|
||||
ReleaseLedgerStock ledger, Nest nest)
|
||||
{
|
||||
var offer = await ledger.OfferAsync(nest, CancellationToken.None);
|
||||
var renest = NestRenest.Run(nest, NestingEngineRegistry.Create("Rectangles"), "Rectangles",
|
||||
offer.Options);
|
||||
return (renest, ledger.Assign(nest, offer, renest.Committed));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LedgerStockIsUsedFirst_EvenWhenTheDefaultSheetWouldBeSmaller()
|
||||
{
|
||||
// One 60x120 ledger sheet holds both parts; a single pass scored by area would take
|
||||
// the smaller 48x96 Nest Defaults sheet instead.
|
||||
var ledger = new FakeLedger { Stock = Stock(Sheet("S-1", 60, 120)) };
|
||||
var nest = NestOf(20, 2);
|
||||
|
||||
var (renest, note) = await Release(ledger.Create(), nest);
|
||||
|
||||
Assert.True(renest.Nested, renest.Note);
|
||||
var plate = Assert.Single(nest.Plates);
|
||||
Assert.Equal(new Size(60, 120), plate.Size);
|
||||
Assert.Equal(["S-1"], plate.StockItemCodes);
|
||||
Assert.Equal(0, nest.Drawings.First().Quantity.Remaining);
|
||||
Assert.Equal("1 sheet from MaterialLedger stock.", note);
|
||||
Assert.Equal((Material, "SYN-1", 0.25), Assert.Single(ledger.StockReads));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CopiesTheLedgerCannotHold_TopUpOnTheNestDefaultsSheet_WithoutACode()
|
||||
{
|
||||
// Two free 40x40 sheets each hold one 30x30 part; the third copy needs a default sheet.
|
||||
var ledger = new FakeLedger { Stock = Stock(Sheet("S-1", 40, 40), Sheet("S-2", 40, 40)) };
|
||||
var nest = NestOf(30, 3);
|
||||
|
||||
var (renest, note) = await Release(ledger.Create(), nest);
|
||||
|
||||
Assert.True(renest.Nested, renest.Note);
|
||||
Assert.Empty(renest.Unplaced);
|
||||
Assert.Equal(0, nest.Drawings.First().Quantity.Remaining);
|
||||
var coded = nest.Plates.Where(p => p.StockItemCodes.Count > 0).ToList();
|
||||
Assert.Equal(["S-1", "S-2"], coded.SelectMany(p => p.StockItemCodes).Order());
|
||||
Assert.All(coded, p => Assert.Equal(new Size(40, 40), p.Size));
|
||||
var topUp = Assert.Single(nest.Plates.Where(p => p.StockItemCodes.Count == 0));
|
||||
Assert.Equal(new Size(48, 96), topUp.Size);
|
||||
Assert.Equal(3, renest.Sheets);
|
||||
Assert.Equal("2 sheets from MaterialLedger stock.", note);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ALaterNestOfTheSameRun_DoesNotGetCodesAnEarlierNestTook()
|
||||
{
|
||||
// The release uploads every nest only after all are nested, so the server cannot
|
||||
// report the first nest's codes yet; a substitute row can list the same sheet twice.
|
||||
var ledger = new FakeLedger { Stock = Stock(Sheet("S-1", 60, 120)) };
|
||||
var run = ledger.Create();
|
||||
await Release(run, NestOf(20, 2));
|
||||
|
||||
var second = NestOf(20, 2, grade: "SYN-2");
|
||||
var (renest, note) = await Release(run, second);
|
||||
|
||||
Assert.True(renest.Nested, renest.Note);
|
||||
var plate = Assert.Single(second.Plates);
|
||||
Assert.Empty(plate.StockItemCodes);
|
||||
Assert.Equal(new Size(48, 96), plate.Size);
|
||||
Assert.Equal("MaterialLedger not used: no free stock of Synthetic Steel SYN-2 0.25 in.",
|
||||
note);
|
||||
Assert.Equal(["S-1"], run.Taken);
|
||||
Assert.Equal(1, ledger.InUseReads);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CodesOnOtherServerNests_AreLeftOut()
|
||||
{
|
||||
var ledger = new FakeLedger
|
||||
{
|
||||
Stock = Stock(Sheet("S-1", 60, 120), Sheet("S-2", 60, 120)),
|
||||
InUse = LedgerCodesInUse.Known(
|
||||
[new NestStockCodeUse { ItemCode = "S-1", NestName = "N26-0099" }]),
|
||||
};
|
||||
|
||||
var offer = await ledger.Create().OfferAsync(NestOf(20, 2), CancellationToken.None);
|
||||
|
||||
Assert.Null(offer.NotUsed);
|
||||
Assert.Equal(["S-2"], Assert.Single(offer.Rows).ItemCodes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AnUnreachableLedger_NestsOnTheNestDefaultsSheet_AndSaysSo()
|
||||
{
|
||||
var ledger = new FakeLedger { Stock = new LedgerStockResult.Unreachable("timed out") };
|
||||
var nest = NestOf(20, 2);
|
||||
|
||||
var (renest, note) = await Release(ledger.Create(), nest);
|
||||
|
||||
Assert.True(renest.Nested, renest.Note);
|
||||
var plate = Assert.Single(nest.Plates);
|
||||
Assert.Equal(new Size(48, 96), plate.Size);
|
||||
Assert.Empty(plate.StockItemCodes);
|
||||
Assert.Equal("MaterialLedger not used: unreachable: timed out.", note);
|
||||
Assert.Equal(0, ledger.InUseReads);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UncheckedOtherNests_AreNamedInTheNote()
|
||||
{
|
||||
var ledger = new FakeLedger
|
||||
{
|
||||
Stock = Stock(Sheet("S-1", 60, 120)),
|
||||
InUse = LedgerCodesInUse.Unknown("the nest server did not answer"),
|
||||
};
|
||||
|
||||
var (_, note) = await Release(ledger.Create(), NestOf(20, 2));
|
||||
|
||||
Assert.Equal("1 sheet from MaterialLedger stock. Codes on other nests were not checked: "
|
||||
+ "the nest server did not answer.", note);
|
||||
}
|
||||
}
|
||||
@@ -111,8 +111,14 @@ public class MainFormReleaseNestingTests
|
||||
&& p.StockItemCodes.Count == 0);
|
||||
Assert.Equal(2, nest.Plates.Sum(p => p.Parts.Count * p.Quantity));
|
||||
Assert.Equal("B3", tab.LedgerLocations["S-1"]);
|
||||
// Each stage is its own Auto Nest step in the tab.
|
||||
// Each stage is its own Auto Nest step in the tab, and its codes are part of that step.
|
||||
Assert.Equal(2, tab.History.UndoCount);
|
||||
tab.Undo();
|
||||
tab.Undo();
|
||||
tab.Redo();
|
||||
tab.Redo();
|
||||
var redone = Assert.Single(tab.Nest.Plates, p => p.StockItemCodes.Count > 0);
|
||||
Assert.Equal("S-1", Assert.Single(redone.StockItemCodes));
|
||||
host.End(null!);
|
||||
Application.DoEvents();
|
||||
}, TestTimeout, "The live release ledger test did not complete.");
|
||||
|
||||
@@ -9,6 +9,7 @@ using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using OpenNest.Data;
|
||||
using OpenNest.Data.MaterialLedger;
|
||||
using OpenNest.Desktop;
|
||||
using OpenNest.Engine.BestFit;
|
||||
using OpenNest.Engine.Jobs;
|
||||
using OpenNest.Forms;
|
||||
@@ -1161,23 +1162,13 @@ namespace OpenNest.Controls
|
||||
var engineName = EngineSelection.EngineName;
|
||||
return Task.Run(async () =>
|
||||
{
|
||||
var offer = ledger == null ? null : await ledger.OfferAsync(nest, token);
|
||||
var renested = NestRenest.Run(nest, NestingEngineRegistry.Create(engineName),
|
||||
engineName, offer?.Options, AutoNestForm.DefaultMinRemnantSize, token: token);
|
||||
var ledgerNote = offer == null
|
||||
? null
|
||||
: ledger.Assign(nest, offer, renested.Committed);
|
||||
return new SetReleaseRenest(renested.Nested,
|
||||
ReleaseNote(renested.Note, ledgerNote));
|
||||
var renested = await ReleaseRenest.RunHeadlessAsync(nest, ledger,
|
||||
NestingEngineRegistry.Create(engineName), engineName,
|
||||
AutoNestForm.DefaultMinRemnantSize, token);
|
||||
return new SetReleaseRenest(renested.Nested, renested.Note);
|
||||
}, token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The note a released nest shows: Auto Nest's, then what MaterialLedger stock it used.
|
||||
/// </summary>
|
||||
internal static string ReleaseNote(string renestNote, string ledgerNote) =>
|
||||
string.Join(" ", new[] { renestNote, ledgerNote }.Where(n => !string.IsNullOrEmpty(n)));
|
||||
|
||||
private static LibraryPackage ReadPackage(byte[] bytes)
|
||||
{
|
||||
var contents = DrawingPackage.Read(bytes);
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Windows.Forms;
|
||||
using OpenNest.Controls;
|
||||
using OpenNest.Data;
|
||||
using OpenNest.Data.MaterialLedger;
|
||||
using OpenNest.Desktop;
|
||||
using OpenNest.Engine.Jobs;
|
||||
using OpenNest.Forms.Commands;
|
||||
|
||||
@@ -54,26 +55,27 @@ namespace OpenNest.Forms
|
||||
if (tab.PlateManager.CanClearAll)
|
||||
tab.ClearNest();
|
||||
nest.UpdateDrawingQuantities();
|
||||
var offer = ledger == null ? null : await ledger.OfferAsync(nest, token);
|
||||
|
||||
using var cts = CancellationTokenSource.CreateLinkedTokenSource(token);
|
||||
// As for Auto Nest, switching tabs while it runs stops it.
|
||||
// As for Auto Nest, switching tabs while it runs (or reads the ledger) stops it.
|
||||
nestingCts = cts;
|
||||
NestRenestResult renested;
|
||||
ReleaseRenestResult renested;
|
||||
try
|
||||
{
|
||||
// Ledger stock first, then the plate defaults: each stage is its own Auto Nest run.
|
||||
// Ledger stock first, then the plate defaults: each stage is its own Auto Nest
|
||||
// run and undo step, with its ledger codes recorded in that step.
|
||||
var solve = SolveReleasedNest
|
||||
?? ((t, r, c) => AutoNestCommand.SolveAsync(this, t, r, c));
|
||||
renested = await NestRenest.RunStagedAsync(nest, EngineSelection.EngineName,
|
||||
offer?.Options, AutoNestForm.DefaultMinRemnantSize,
|
||||
renested = await ReleaseRenest.RunAsync(nest, ledger, EngineSelection.EngineName,
|
||||
AutoNestForm.DefaultMinRemnantSize,
|
||||
request => solve(tab, request, cts),
|
||||
r =>
|
||||
(r, finish) =>
|
||||
{
|
||||
using var change = tab.BeginChange("Auto Nest");
|
||||
change.MarkChanged();
|
||||
var applied = NestPipelineCommit.ApplyToEmptyPlates(r, tab.PlateManager,
|
||||
token: cts.Token);
|
||||
finish(applied);
|
||||
change.Complete();
|
||||
return applied;
|
||||
}, cts.Token);
|
||||
@@ -83,20 +85,15 @@ namespace OpenNest.Forms
|
||||
nestingCts = null;
|
||||
}
|
||||
|
||||
var ledgerNote = offer == null ? null : ledger.Assign(nest, offer, renested.Committed);
|
||||
foreach (var row in offer?.Rows ?? Array.Empty<LedgerStockRow>())
|
||||
{
|
||||
foreach (var (code, location) in row.ItemLocations)
|
||||
tab.LedgerLocations[code] = location;
|
||||
}
|
||||
foreach (var (code, location) in renested.LedgerLocations)
|
||||
tab.LedgerLocations[code] = location;
|
||||
if (renested.Nested)
|
||||
{
|
||||
tab.UpdatePlateList();
|
||||
tab.PlateManager.LoadFirst();
|
||||
}
|
||||
tab.PlateView.ZoomToFit();
|
||||
return new SetReleaseRenest(renested.Nested,
|
||||
ReleasePanel.ReleaseNote(renested.Note, ledgerNote));
|
||||
return new SetReleaseRenest(renested.Nested, renested.Note);
|
||||
}
|
||||
|
||||
void IReleaseNestingHost.End(SetReleaseResult result)
|
||||
|
||||
Reference in new issue
Block a user