diff --git a/OpenNest.Tests/Engine/ClampRailWorkflowTests.cs b/OpenNest.Tests/Engine/ClampRailWorkflowTests.cs new file mode 100644 index 0000000..136a633 --- /dev/null +++ b/OpenNest.Tests/Engine/ClampRailWorkflowTests.cs @@ -0,0 +1,128 @@ +using OpenNest.CNC; +using OpenNest.Data; +using OpenNest.Engine; +using OpenNest.Engine.Jobs; +using OpenNest.Geometry; +using OpenNest.IO; + +namespace OpenNest.Tests.Engine; + +/// +/// The desktop clamp workflow end to end without the form: enabled machine settings give a +/// new nest its rail, whole-job Auto Nest (template stock, validated pipeline, commit) keeps +/// every part clear of the clamps on every sheet while still using the edge between them, +/// and save/reload preserves clamps and placements without writing the empty tail sheet. +/// +public class ClampRailWorkflowTests +{ + private const double EdgeSpacing = 0.25; + private static readonly double[] Centers = { 3, 10.5, 18 }; + + private static Drawing Block() + { + var program = new Program(); + program.MoveTo(0, 0); + program.LineTo(4, 0); + program.LineTo(4, 2); + program.LineTo(0, 2); + program.LineTo(0, 0); + return new Drawing("neutral-block", program); + } + + private static Nest NewNest(int quadrant) + { + var nest = new Nest { Units = Units.Inches }; + nest.PlateDefaults.Size = new Size(12, 24); // 12 deep (Y), 24 along the clamp edge (X) + nest.PlateDefaults.Quadrant = quadrant; + nest.PlateDefaults.EdgeSpacing = new Spacing(EdgeSpacing, EdgeSpacing, EdgeSpacing, EdgeSpacing); + nest.PlateDefaults.PartSpacing = 0.25; + new ClampSettings { Enabled = true, Centers = Centers }.ApplyToNewNest(nest); + return nest; + } + + /// Independent oracle: part boxes against clamps grown by the bottom edge spacing. + private static void AssertClearOfClamps(Plate plate) + { + var left = plate.Quadrant is 2 or 3 ? -plate.Size.Length : 0; + var bottom = plate.Quadrant is 3 or 4 ? -plate.Size.Width : 0; + Assert.NotEmpty(plate.ClampZones); + foreach (var zone in plate.ClampZones) + { + var keepOut = zone.Bounds.Translate(left, bottom); + foreach (var part in plate.Parts) + { + var box = part.BoundingBox; + var overlapX = System.Math.Min(box.Right, keepOut.Right + EdgeSpacing) + - System.Math.Max(box.Left, keepOut.Left - EdgeSpacing); + var overlapY = System.Math.Min(box.Top, keepOut.Top + EdgeSpacing) + - System.Math.Max(box.Bottom, keepOut.Bottom - EdgeSpacing); + Assert.False(overlapX > 1e-6 && overlapY > 1e-6, + $"{part.BaseDrawing.Name} at {box} enters {zone.Id} at {keepOut}"); + } + } + } + + [Theory] + [InlineData("Rectangles", 1)] + [InlineData("Default", 1)] + [InlineData("Rectangles", 3)] + public void AutoNestAvoidsMachineClampsAndTheyRoundTrip(string engine, int quadrant) + { + const int quantity = 40; + var drawing = Block(); + var nest = NewNest(quadrant); + nest.Drawings.Add(drawing); + var rail = nest.PlateDefaults.ClampRail; + Assert.NotNull(rail); + using var manager = new PlateManager(nest); + var template = manager.GetOrCreateEmpty(); + Assert.Equal(3, template.ClampZones.Count); + + var item = new NestItem + { + Drawing = drawing, + Quantity = quantity, + StepAngle = OpenNest.Math.Angle.HalfPI, + RotationStart = 0, + RotationEnd = OpenNest.Math.Angle.HalfPI, + }; + var result = NestPipeline.Run(new NestPipelineRequest(engine, [item], + NestStockBuilder.FromTemplate(template, null))); + Assert.True(result.IsValid, string.Join("; ", result.Violations)); + Assert.Equal(quantity, Assert.Single(result.Raw.Fulfillment).Placed); + + var applied = NestPipelineCommit.ApplyToEmptyPlates(result, manager); + + Assert.True(applied.Count >= 2, $"expected several sheets, got {applied.Count}"); + foreach (var plate in applied) + { + Assert.Same(rail, plate.ClampRail); + Assert.True(rail.Matches(plate)); + AssertClearOfClamps(plate); + } + if (quadrant == 1) + { + // The edge between clamps stays usable: some part sits in the clamp band. + Assert.Contains(applied.SelectMany(p => p.Parts), + p => p.BoundingBox.Bottom < 0.625 + EdgeSpacing); + } + + using var stream = new MemoryStream(); + Assert.True(new NestWriter(nest).Write(stream)); + var loaded = new NestReader(new MemoryStream(stream.ToArray())).Read(); + + Assert.Equal(rail, loaded.PlateDefaults.ClampRail); + Assert.Equal(applied.Count, loaded.Plates.Count); // trailing empty sheet not written + for (var i = 0; i < applied.Count; i++) + { + var reloaded = loaded.Plates[i]; + Assert.Same(loaded.PlateDefaults.ClampRail, reloaded.ClampRail); + Assert.Equal(applied[i].ClampZones.Select(z => (z.Id, z.Bounds.X, z.Bounds.Y, z.Bounds.Length, z.Bounds.Width)), + reloaded.ClampZones.Select(z => (z.Id, z.Bounds.X, z.Bounds.Y, z.Bounds.Length, z.Bounds.Width))); + Assert.Equal(applied[i].Parts.Select(p => (p.Location, p.Rotation)), + reloaded.Parts.Select(p => (p.Location, p.Rotation))); + AssertClearOfClamps(reloaded); + } + Assert.Equal(3, loaded.CreatePlate().ClampZones.Count); + } +}