diff --git a/OpenNest.Desktop.Tests/AutoNestJobTests.cs b/OpenNest.Desktop.Tests/AutoNestJobTests.cs new file mode 100644 index 00000000..a8c8dbd0 --- /dev/null +++ b/OpenNest.Desktop.Tests/AutoNestJobTests.cs @@ -0,0 +1,93 @@ +using OpenNest.CNC; +using OpenNest.Engine.Jobs; +using OpenNest.Geometry; + +namespace OpenNest.Desktop.Tests; + +public class AutoNestJobTests +{ + private static Drawing Square(string name, double side, int required) + { + var program = new Program(); + program.Codes.Add(new RapidMove(new Vector(0, 0))); + program.Codes.Add(new LinearMove(new Vector(side, 0))); + program.Codes.Add(new LinearMove(new Vector(side, side))); + program.Codes.Add(new LinearMove(new Vector(0, side))); + program.Codes.Add(new LinearMove(new Vector(0, 0))); + var drawing = new Drawing(name, program); + drawing.Quantity.Required = required; + return drawing; + } + + private static Nest NestWith(params Drawing[] drawings) + { + var nest = new Nest { Name = "job" }; + foreach (var drawing in drawings) + nest.Drawings.Add(drawing); + nest.Plates.Add(new Plate(new Size(48, 96)) { Quantity = 1 }); + return nest; + } + + [Fact] + public void ForRemaining_AsksForWhatIsNotPlacedYet_OnTheTemplateStock() + { + var square = Square("square", 10, 5); + var nest = NestWith(square); + nest.Plates[0].Parts.Add(new Part(square)); + + var request = AutoNestJob.ForRemaining(nest, nest.Plates[0], "Default"); + + var item = Assert.Single(request.Items); + Assert.Same(square, item.Drawing); + Assert.Equal(4, item.Quantity); + Assert.Equal("Default", request.EngineName); + Assert.Equal(nest.Plates[0].Size, Assert.Single(request.Stock).Size); + } + + [Fact] + public void ForRemaining_SkipsCutOffsAndPlacedDrawings_AndIsNullWhenNothingRemains() + { + var placed = Square("placed", 10, 1); + var cutOff = Square("cut-off", 1, 3); + cutOff.IsCutOff = true; + var nest = NestWith(placed, cutOff); + nest.Plates[0].Parts.Add(new Part(placed)); + + Assert.Null(AutoNestJob.ForRemaining(nest, nest.Plates[0], "Default")); + } + + [Fact] + public void ValidResult_IsCommittedOntoTheEmptyPlate() + { + var square = Square("square", 10, 6); + var nest = NestWith(square); + var request = AutoNestJob.ForRemaining(nest, nest.Plates[0], "Default"); + + var result = NestPipeline.Run(request); + var kept = AutoNestJob.Keep(nest, result); + + Assert.True(kept.Nested, kept.Note); + Assert.Null(AutoNestJob.RefusalReason(result)); + var plate = Assert.Single(nest.Plates); + Assert.Equal(6, plate.Parts.Count); + Assert.Equal(0, square.Quantity.Remaining); + } + + [Fact] + public void ResultPlacingNothing_LeavesTheNestUnchanged() + { + var tooBig = Square("too big", 200, 1); + var nest = NestWith(tooBig); + var request = AutoNestJob.ForRemaining(nest, nest.Plates[0], "Default"); + + var result = NestPipeline.Run(request); + var kept = AutoNestJob.Keep(nest, result); + + Assert.False(kept.Nested); + var reason = AutoNestJob.RefusalReason(result); + Assert.StartsWith("Auto Nest placed nothing; nothing was changed. Not placed: ", reason); + Assert.Contains("too big", reason); + Assert.Empty(Assert.Single(nest.Plates).Parts); + Assert.Equal(1, tooBig.Quantity.Remaining); + } +} diff --git a/OpenNest.Desktop/AutoNestJob.cs b/OpenNest.Desktop/AutoNestJob.cs new file mode 100644 index 00000000..b3a640e4 --- /dev/null +++ b/OpenNest.Desktop/AutoNestJob.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using OpenNest.Engine; +using OpenNest.Engine.Jobs; + +namespace OpenNest.Desktop +{ + /// + /// Auto Nest with the desktop dialog's defaults and no dialog: every drawing's remaining + /// quantity, with its priority and rotation constraints, on sheets like a template plate, + /// through . Only a valid, keepable result that places something + /// is committed, onto the nest's empty plates first and then new plates from its defaults. + /// + public static class AutoNestJob + { + /// + /// The job for 's remaining quantities on stock like + /// ; null when nothing remains. The nest is not changed, so + /// the caller can solve it off its thread. + /// + public static NestPipelineRequest ForRemaining(Nest nest, Plate template, string engineName) + { + ArgumentNullException.ThrowIfNull(nest); + ArgumentNullException.ThrowIfNull(template); + var items = nest.Drawings + .Where(d => !d.IsCutOff && d.Quantity.Remaining > 0) + .Select(d => new NestItem + { + Drawing = d, + Quantity = d.Quantity.Remaining, + Priority = d.Priority, + RotationStart = d.Constraints.StartAngle, + RotationEnd = d.Constraints.EndAngle, + StepAngle = d.Constraints.StepAngle, + }) + .ToList(); + if (items.Count == 0) + return null; + + return new NestPipelineRequest(engineName, items, + NestStockBuilder.FromTemplate(template, null), + new NestJobOptions(maxPlates: NestRenest.MaxPlates, salvageRate: 0, + minimumSalvageDimension: nest.MinimumRemnantDimension)); + } + + /// + /// Commits to on the nest's own thread + /// when it is valid, keepable and places something; otherwise leaves the nest unchanged + /// and says why in the note. + /// + public static NestRenestResult Keep(Nest nest, NestPipelineResult result) + { + ArgumentNullException.ThrowIfNull(nest); + return NestRenest.Keep(result, + valid => NestPipelineCommit.ApplyToPlates(valid, () => EmptyOrNewPlate(nest))); + } + + private static Plate EmptyOrNewPlate(Nest nest) => + nest.Plates.FirstOrDefault(p => p.Parts.Count == 0) ?? nest.CreatePlate(); + + /// + /// Why would commit nothing for , naming the + /// first problem; null when it would commit. + /// + public static string RefusalReason(NestPipelineResult result) + { + ArgumentNullException.ThrowIfNull(result); + if (!result.IsValid) + return $"Auto Nest found {result.Violations.Count} layout problem" + + $"{(result.Violations.Count == 1 ? "" : "s")} (first: {result.Violations[0]}); nothing was changed."; + if (!result.CanKeep) + return "Auto Nest's layout cannot be kept faithfully; nothing was changed."; + if (result.Plates.All(p => p.Parts.Count == 0)) + return "Auto Nest placed nothing; nothing was changed." + + (result.Unplaced.Count > 0 + ? " Not placed: " + string.Join("; ", result.Unplaced.Select(u => u.Describe())) + "." + : ""); + return null; + } + } +}