From ffe32fc38c75b4bb60993b46f3e8aa196fde1df3 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Tue, 31 Mar 2026 17:39:14 -0400 Subject: [PATCH] test: add lead-in rotation preservation tests Cover assign, remove, re-assign, multiple rotations, and external HasManualLeadIns scenarios to verify rotation is preserved throughout the lead-in lifecycle. Co-Authored-By: Claude Opus 4.6 (1M context) --- OpenNest.Tests/LeadInAssignerTests.cs | 256 ++++++++++++++++++++++++++ 1 file changed, 256 insertions(+) diff --git a/OpenNest.Tests/LeadInAssignerTests.cs b/OpenNest.Tests/LeadInAssignerTests.cs index 877206d..a3bb72c 100644 --- a/OpenNest.Tests/LeadInAssignerTests.cs +++ b/OpenNest.Tests/LeadInAssignerTests.cs @@ -95,4 +95,260 @@ public class LeadInAssignerTests var hasLeadin = plate.Parts[0].Program.Codes.OfType().Any(m => m.Layer == LayerType.Leadin); Assert.True(hasLeadin); } + + [Fact] + public void Assign_PreservesRotationOnRotatedParts() + { + var drawing = MakeSquareDrawing(); + var rotation = System.Math.PI / 2; // 90 degrees + var part = Part.CreateAtOrigin(drawing, rotation); + part.Offset(10, 10); + + var plate = new Plate(60, 120); + plate.Parts.Add(part); + plate.CuttingParameters = new CuttingParameters + { + ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 } + }; + + var assigner = new LeadInAssigner { Sequencer = new LeftSideSequencer() }; + assigner.Assign(plate); + + Assert.Equal(rotation, part.Rotation, 6); + } + + [Fact] + public void Assign_ThenRemove_PreservesRotation() + { + var drawing = MakeSquareDrawing(); + var rotation = System.Math.PI / 4; // 45 degrees + var part = Part.CreateAtOrigin(drawing, rotation); + part.Offset(15, 15); + + var plate = new Plate(60, 120); + plate.Parts.Add(part); + plate.CuttingParameters = new CuttingParameters + { + ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 } + }; + + var assigner = new LeadInAssigner { Sequencer = new LeftSideSequencer() }; + assigner.Assign(plate); + Assert.Equal(rotation, part.Rotation, 6); + + part.RemoveLeadIns(); + Assert.Equal(rotation, part.Rotation, 6); + } + + [Fact] + public void Assign_PreservesLocationOnRotatedParts() + { + var drawing = MakeSquareDrawing(); + var part = Part.CreateAtOrigin(drawing, System.Math.PI / 2); + part.Offset(20, 30); + var location = part.Location; + + var plate = new Plate(60, 120); + plate.Parts.Add(part); + plate.CuttingParameters = new CuttingParameters + { + ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 } + }; + + var assigner = new LeadInAssigner { Sequencer = new LeftSideSequencer() }; + assigner.Assign(plate); + + Assert.Equal(location.X, part.Location.X, 6); + Assert.Equal(location.Y, part.Location.Y, 6); + } + + [Fact] + public void Assign_ThenRemove_RestoresBoundingBox() + { + var drawing = MakeSquareDrawing(); + var rotation = System.Math.PI / 2; + var part = Part.CreateAtOrigin(drawing, rotation); + part.Offset(10, 10); + var originalBbox = part.BoundingBox; + + var plate = new Plate(60, 120); + plate.Parts.Add(part); + plate.CuttingParameters = new CuttingParameters + { + ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 } + }; + + var assigner = new LeadInAssigner { Sequencer = new LeftSideSequencer() }; + assigner.Assign(plate); + part.RemoveLeadIns(); + + Assert.Equal(originalBbox.X, part.BoundingBox.X, 4); + Assert.Equal(originalBbox.Y, part.BoundingBox.Y, 4); + Assert.Equal(originalBbox.Width, part.BoundingBox.Width, 4); + Assert.Equal(originalBbox.Length, part.BoundingBox.Length, 4); + } + + [Fact] + public void Assign_ProgramGeometryMatchesRotatedOriginal() + { + var drawing = MakeSquareDrawing(); + var rotation = System.Math.PI / 2; + var part = Part.CreateAtOrigin(drawing, rotation); + part.Offset(10, 10); + + // Get the original rotated geometry + var originalGeometry = OpenNest.Converters.ConvertProgram.ToGeometry(part.Program); + var originalNonRapid = originalGeometry.Where(e => e.Layer != SpecialLayers.Rapid).ToList(); + + var plate = new Plate(60, 120); + plate.Parts.Add(part); + plate.CuttingParameters = new CuttingParameters + { + ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 } + }; + + var assigner = new LeadInAssigner { Sequencer = new LeftSideSequencer() }; + assigner.Assign(plate); + + // The lead-in program should produce geometry that contains the + // original rotated shape (plus lead-in/out extensions) + var leadInGeometry = OpenNest.Converters.ConvertProgram.ToGeometry(part.Program); + var leadInNonRapid = leadInGeometry.Where(e => + e.Layer != SpecialLayers.Rapid && + e.Layer != SpecialLayers.Leadin && + e.Layer != SpecialLayers.Leadout).ToList(); + + // The bounding box of the cut geometry should be close to original + var origBbox = GetEntityBounds(originalNonRapid); + var leadBbox = GetEntityBounds(leadInNonRapid); + + Assert.Equal(origBbox.Width, leadBbox.Width, 2); + Assert.Equal(origBbox.Length, leadBbox.Length, 2); + } + + [Fact] + public void Assign_MultipleRotatedParts_AllPreserveRotation() + { + var drawing = MakeSquareDrawing(); + var rotations = new[] { 0.0, System.Math.PI / 6, System.Math.PI / 4, System.Math.PI / 2 }; + + var plate = new Plate(60, 120); + for (var i = 0; i < rotations.Length; i++) + { + var part = Part.CreateAtOrigin(drawing, rotations[i]); + part.Offset(10 + i * 15, 10); + plate.Parts.Add(part); + } + + plate.CuttingParameters = new CuttingParameters + { + ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 } + }; + + var assigner = new LeadInAssigner { Sequencer = new LeftSideSequencer() }; + assigner.Assign(plate); + + for (var i = 0; i < rotations.Length; i++) + Assert.Equal(rotations[i], plate.Parts[i].Rotation, 6); + } + + [Fact] + public void Assign_Twice_PreservesRotation() + { + var drawing = MakeSquareDrawing(); + var rotation = System.Math.PI / 3; // 60 degrees + var part = Part.CreateAtOrigin(drawing, rotation); + part.Offset(10, 10); + + var plate = new Plate(60, 120); + plate.Parts.Add(part); + plate.CuttingParameters = new CuttingParameters + { + ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 } + }; + + var assigner = new LeadInAssigner { Sequencer = new LeftSideSequencer() }; + assigner.Assign(plate); + Assert.Equal(rotation, part.Rotation, 6); + + // Assign again (this removes first, then re-applies) + assigner.Assign(plate); + Assert.Equal(rotation, part.Rotation, 6); + } + + [Fact] + public void Assign_AfterExternalHasManualLeadIns_PreservesRotation() + { + // Simulates loading a nest where HasManualLeadIns was saved as true + // but the program doesn't actually contain lead-in codes. + var drawing = MakeSquareDrawing(); + var rotation = System.Math.PI / 2; + var part = Part.CreateAtOrigin(drawing, rotation); + part.Offset(10, 10); + part.HasManualLeadIns = true; // externally set (e.g., by NestReader) + + var plate = new Plate(60, 120); + plate.Parts.Add(part); + plate.CuttingParameters = new CuttingParameters + { + ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 } + }; + + var assigner = new LeadInAssigner { Sequencer = new LeftSideSequencer() }; + assigner.Assign(plate); + + Assert.Equal(rotation, part.Rotation, 6); + } + + [Fact] + public void RemoveLeadIns_AfterExternalHasManualLeadIns_PreservesRotation() + { + // Simulates the case where HasManualLeadIns is set externally + // and then lead-ins are removed. + var drawing = MakeSquareDrawing(); + var rotation = System.Math.PI / 3; + var part = Part.CreateAtOrigin(drawing, rotation); + part.Offset(10, 10); + part.HasManualLeadIns = true; + + part.RemoveLeadIns(); + + Assert.Equal(rotation, part.Rotation, 6); + } + + private static Drawing MakeSquareDrawing() + { + var pgm = new Program(); + pgm.Codes.Add(new RapidMove(new Vector(0, 0))); + pgm.Codes.Add(new LinearMove(new Vector(0, 10))); + pgm.Codes.Add(new LinearMove(new Vector(10, 10))); + pgm.Codes.Add(new LinearMove(new Vector(10, 0))); + pgm.Codes.Add(new LinearMove(new Vector(0, 0))); + return new Drawing("test", pgm); + } + + private static Box GetEntityBounds(List entities) + { + double minX = double.MaxValue, minY = double.MaxValue; + double maxX = double.MinValue, maxY = double.MinValue; + + foreach (var entity in entities) + { + if (entity is OpenNest.Geometry.Line line) + { + UpdateBounds(line.StartPoint, ref minX, ref minY, ref maxX, ref maxY); + UpdateBounds(line.EndPoint, ref minX, ref minY, ref maxX, ref maxY); + } + } + + return new Box(minX, minY, maxX - minX, maxY - minY); + } + + private static void UpdateBounds(Vector pt, ref double minX, ref double minY, ref double maxX, ref double maxY) + { + if (pt.X < minX) minX = pt.X; + if (pt.Y < minY) minY = pt.Y; + if (pt.X > maxX) maxX = pt.X; + if (pt.Y > maxY) maxY = pt.Y; + } }