mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-27 19:41:08 -04:00
Auto Nest on MaterialLedger stock now records which inventory item each
physical sheet uses, so ML can hold exactly those items instead of matching
by size.
- Core: Plate.StockItemCodes (entry i = sheet i of Quantity; empty = uncoded).
Core invalidates it: a resize clears it (turning the sheet keeps it), a lower
Quantity truncates it, a nest material/grade/thickness change clears every
plate's, ClearAll clears it and new plates never copy it.
- Desktop Auto Nest: after a kept proposal is committed, LedgerStockAssignment
deals each ledger row's codes to its plates in Nest.Plates order (bundles
repeat their code; clamp-removal variants use their base row). Rejected,
discarded or cancelled proposals never reach it. NestPipelineCommit.PairWithStock
pairs committed plates with their solved stock; the engine stays ledger-free.
- Plate list tooltip: "Stock: CODE (location)"; locations are display-only.
- nest.json: optional plate stockItemCodes, omitted when empty, read leniently.
- /api/nests/{id}/sheets: new plates[] { index, width, length, quantity,
stockItemCodes }; sheets[] unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
170 lines
6.6 KiB
C#
170 lines
6.6 KiB
C#
using OpenNest.Data.MaterialLedger;
|
|
using OpenNest.Engine;
|
|
using OpenNest.Engine.Jobs;
|
|
using OpenNest.Geometry;
|
|
using OpenNest.Shapes;
|
|
|
|
namespace OpenNest.Tests.Data;
|
|
|
|
/// <summary>
|
|
/// After Auto Nest commits, each plate on ledger stock takes the next codes of its row in nest
|
|
/// order; nothing else touches codes, so a rejected proposal leaves them as they were.
|
|
/// </summary>
|
|
public class LedgerStockAssignmentTests
|
|
{
|
|
private static LedgerStockRow Row(string id, double width, double length, params string[] codes) =>
|
|
new(id, new Size(width, length), codes.Length, IsDrop: codes.Distinct().Count() > 1, IsIrregular: false,
|
|
codes, Array.Empty<string>());
|
|
|
|
[Fact]
|
|
public void Plates_DequeueTheirRowsCodes_InNestOrder_AndBundlesRepeatTheirCode()
|
|
{
|
|
var drops = Row("ledger:1", 30, 52.5, "DROP-1182", "DROP-1190", "DROP-1201");
|
|
var bundle = Row("ledger:2", 60, 120, "SHEET-456", "SHEET-456", "SHEET-456");
|
|
var nest = new Nest();
|
|
var first = Add(nest, 1);
|
|
var second = Add(nest, 2);
|
|
var third = Add(nest, 2);
|
|
var hand = Add(nest, 1);
|
|
hand.StockItemCodes = ["STALE"];
|
|
|
|
// Committed out of nest order: codes still follow Nest.Plates.
|
|
LedgerStockAssignment.Assign(nest.Plates,
|
|
[(third, "ledger:2"), (first, "ledger:1"), (second, "ledger:1"), (hand, "option-0")],
|
|
[drops, bundle]);
|
|
|
|
Assert.Equal(["DROP-1182"], first.StockItemCodes);
|
|
Assert.Equal(["DROP-1190", "DROP-1201"], second.StockItemCodes);
|
|
Assert.Equal(["SHEET-456", "SHEET-456"], third.StockItemCodes);
|
|
Assert.Empty(hand.StockItemCodes);
|
|
}
|
|
|
|
[Fact]
|
|
public void ARowThatRunsOut_LeavesTheRemainingSheetsUncoded()
|
|
{
|
|
var nest = new Nest();
|
|
var first = Add(nest, 2);
|
|
var second = Add(nest, 1);
|
|
|
|
LedgerStockAssignment.Assign(nest.Plates, [(first, "ledger:1"), (second, "ledger:1")],
|
|
[Row("ledger:1", 30, 52.5, "DROP-1")]);
|
|
|
|
Assert.Equal(["DROP-1"], first.StockItemCodes);
|
|
Assert.Empty(second.StockItemCodes);
|
|
}
|
|
|
|
[Fact]
|
|
public void ASheetSolvedWithClampsRemoved_TakesItsBaseRowsCodes()
|
|
{
|
|
var nest = new Nest();
|
|
var plate = Add(nest, 1);
|
|
|
|
LedgerStockAssignment.Assign(nest.Plates, [(plate, "ledger:3::removed:clamp-2")],
|
|
[Row("ledger:3", 48, 96, "SHEET-9")]);
|
|
|
|
Assert.Equal(["SHEET-9"], plate.StockItemCodes);
|
|
Assert.Equal("ledger:3", LedgerStockAssignment.BaseStockId("ledger:3::removed:clamp-2,clamp-3"));
|
|
}
|
|
|
|
[Fact]
|
|
public void CommittedProposal_CodesThePlatesItWasSolvedOn()
|
|
{
|
|
var (nest, drawing, manager) = Workspace();
|
|
using (manager)
|
|
{
|
|
var rows = new[] { Row("ledger:1", 30, 52.5, "DROP-1", "DROP-2") };
|
|
var result = Solve(drawing, overlap: false);
|
|
Assert.True(result.IsValid, string.Join("; ", result.Violations));
|
|
|
|
var applied = NestPipelineCommit.ApplyToEmptyPlates(result, manager);
|
|
LedgerStockAssignment.Assign(nest.Plates,
|
|
NestPipelineCommit.PairWithStock(result, applied).Select(p => (p.Plate, p.Stock.Id)), rows);
|
|
|
|
Assert.Equal(["DROP-1"], Assert.Single(applied).StockItemCodes);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void RejectedProposal_LeavesCodesUntouched()
|
|
{
|
|
var (nest, drawing, manager) = Workspace();
|
|
using (manager)
|
|
{
|
|
var empty = nest.Plates.Single();
|
|
empty.StockItemCodes = ["SHEET-OLD"];
|
|
var result = Solve(drawing, overlap: true);
|
|
Assert.False(result.IsValid);
|
|
|
|
// Auto Nest assigns only after a commit; without consent there is none.
|
|
Assert.Throws<InvalidOperationException>(() => NestPipelineCommit.ApplyToEmptyPlates(result, manager));
|
|
|
|
Assert.Equal(["SHEET-OLD"], empty.StockItemCodes);
|
|
Assert.Empty(empty.Parts);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Describe_NamesEachCodeWithItsLocation_CountsRuns_AndNotesUncodedSheets()
|
|
{
|
|
var plate = new Plate(60, 120) { Quantity = 4 };
|
|
Assert.Null(LedgerStockAssignment.Describe(plate, null));
|
|
|
|
plate.StockItemCodes = ["SHEET-456", "SHEET-456", "DROP-1182"];
|
|
var locations = new Dictionary<string, string> { ["SHEET-456"] = "A1" };
|
|
|
|
Assert.Equal("Stock: SHEET-456 (A1) x2, DROP-1182, 1 uncoded", LedgerStockAssignment.Describe(plate, locations));
|
|
}
|
|
|
|
[Fact]
|
|
public void Mapper_KeepsEachItemsLocationForDisplay()
|
|
{
|
|
var mapping = LedgerStockMapper.Map(
|
|
[
|
|
new LedgerStockItem("D-1", 1, LedgerItemKinds.Drop, 24, 36, 1, LedgerItemShapes.Rectangular,
|
|
"R1", "Rack 1", null, null, DateTime.UtcNow),
|
|
new LedgerStockItem("D-2", 1, LedgerItemKinds.Drop, 24, 36, 1, LedgerItemShapes.Rectangular,
|
|
"R2", "Rack 2", null, null, DateTime.UtcNow),
|
|
], Units.Inches);
|
|
|
|
var row = Assert.Single(mapping.Rows);
|
|
Assert.Equal("R1", row.ItemLocations["D-1"]);
|
|
Assert.Equal("R2", row.ItemLocations["D-2"]);
|
|
}
|
|
|
|
private static Plate Add(Nest nest, int quantity)
|
|
{
|
|
var plate = new Plate(30, 52.5) { Quantity = quantity };
|
|
nest.Plates.Add(plate);
|
|
return plate;
|
|
}
|
|
|
|
private static (Nest Nest, Drawing Drawing, PlateManager Manager) Workspace()
|
|
{
|
|
var nest = new Nest();
|
|
var drawing = new RectangleShape { Name = "SYN", Length = 4, Width = 2 }.GetDrawing();
|
|
nest.Drawings.Add(drawing);
|
|
nest.CreatePlate();
|
|
return (nest, drawing, new PlateManager(nest));
|
|
}
|
|
|
|
private static NestPipelineResult Solve(Drawing drawing, bool overlap) =>
|
|
NestPipeline.Run(new OnFirstStock(overlap), "stub", new NestPipelineRequest("stub",
|
|
[new NestItem { Drawing = drawing, Quantity = 2 }],
|
|
[new NestPlateStock("ledger:1", new Size(30, 52.5), 2, 0.25, new Spacing(1, 1), 1)]));
|
|
|
|
// Two copies on one sheet of the first stock row; overlapping when asked, so validation fails.
|
|
private sealed class OnFirstStock(bool overlap) : INestingEngine
|
|
{
|
|
public NestJobResult Solve(NestJob job, IProgress<NestJobProgress> progress = null,
|
|
CancellationToken token = default) => new(
|
|
NestJobStatus.Complete, NestJobStopReason.Completed,
|
|
[new NestJobPlateResult(0, job.Plates[0],
|
|
[
|
|
new NestJobPlacement(job.Parts[0].Id, 0, 2, 2, 0),
|
|
new NestJobPlacement(job.Parts[0].Id, 1, overlap ? 2 : 20, 2, 0),
|
|
])],
|
|
[new PartFulfillment(job.Parts[0].Id, 2, 2, 0)],
|
|
[new StockUsage(job.Plates[0].Id, 1, 1)]);
|
|
}
|
|
}
|