Files
OpenNest/OpenNest.Tests/CuttingStrategy/PartLeadInTransformTests.cs
T
aj 20395eed9b fix(core): keep lead-in state and rotation through part copy and rotate
Part.Clone and CloneAtOffset copied the lead-in program but not
HasManualLeadIns, LeadInsLocked, CuttingParameters or the tracked
pre-lead-in rotation. A copy of a rotated lead-in part reported
rotation 0 (the rebuilt program's), so saving it wrote the wrong
rotation and Remove Lead-ins restored an unrotated part.

Part.Rotate on a lead-in part set the tracked rotation from the
lead-in program's own Rotation, which starts at zero when the cutting
strategy rebuilds it, so a further quarter turn left Rotation unchanged.
Lead-in parts now accumulate the applied angle instead.
2026-09-28 18:22:36 -04:00

186 lines
6.4 KiB
C#

using OpenNest.CNC;
using OpenNest.CNC.CuttingStrategy;
using OpenNest.Geometry;
namespace OpenNest.Tests.CuttingStrategy;
/// <summary>
/// Copying and rotating a part that already carries lead-ins must keep its true
/// rotation, lead-in state and geometry, so Remove Lead-ins (and saving) still
/// see the part the user placed.
/// </summary>
public class PartLeadInTransformTests
{
private const double QuarterTurn = System.Math.PI / 2;
/// <summary>10x10 square with a radius-1 hole at (5, 5), so lead-in
/// assignment emits both a perimeter lead-in and a hole sub-program.</summary>
private static Drawing MakeSquareWithHole()
{
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)));
pgm.Codes.Add(new RapidMove(new Vector(6, 5)));
pgm.Codes.Add(new ArcMove(new Vector(6, 5), new Vector(5, 5), RotationType.CW));
return new Drawing("square-with-hole", pgm);
}
private static CuttingParameters Parameters() =>
new()
{
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 },
ArcCircleLeadIn = new LineLeadIn { Length = 0.3, ApproachAngle = 90 },
};
private static Part MakeLeadInPart(Drawing drawing, double rotation)
{
var part = new Part(drawing);
part.Rotate(rotation);
part.Offset(20, 5);
part.ApplyLeadIns(Parameters(), new Vector(-5, -5));
return part;
}
/// <summary>Clean part of the drawing at the given rotation and location: what
/// Remove Lead-ins must restore.</summary>
private static Part MakeCleanPart(Drawing drawing, double rotation, Vector location)
{
var part = new Part(drawing);
part.Rotate(rotation);
part.Location = location;
return part;
}
private static void AssertSameBox(Box expected, Box actual)
{
Assert.Equal(expected.X, actual.X, 6);
Assert.Equal(expected.Y, actual.Y, 6);
Assert.Equal(expected.Length, actual.Length, 6);
Assert.Equal(expected.Width, actual.Width, 6);
}
private static int LeadInMoveCount(Program program) =>
program.Codes.OfType<LinearMove>().Count(m => m.Layer == LayerType.Leadin)
+ program.Codes.OfType<ArcMove>().Count(m => m.Layer == LayerType.Leadin);
[Fact]
public void Clone_KeepsLeadInStateAndRotation()
{
var drawing = MakeSquareWithHole();
var source = MakeLeadInPart(drawing, QuarterTurn);
source.LeadInsLocked = true;
var clone = (Part)source.Clone();
Assert.True(clone.HasManualLeadIns);
Assert.True(clone.LeadInsLocked);
Assert.Same(source.CuttingParameters, clone.CuttingParameters);
Assert.Equal(source.Rotation, clone.Rotation, 6);
Assert.Equal(LeadInMoveCount(source.Program), LeadInMoveCount(clone.Program));
AssertSameBox(source.BoundingBox, clone.BoundingBox);
}
[Fact]
public void CloneAtOffset_KeepsLeadInStateAndRotation()
{
var drawing = MakeSquareWithHole();
var source = MakeLeadInPart(drawing, QuarterTurn);
var copy = source.CloneAtOffset(new Vector(3, 4));
Assert.True(copy.HasManualLeadIns);
Assert.Equal(source.Rotation, copy.Rotation, 6);
Assert.Equal(source.Location.X + 3, copy.Location.X, 6);
Assert.Equal(source.Location.Y + 4, copy.Location.Y, 6);
}
[Fact]
public void Clone_ThenRemoveLeadIns_RestoresCleanRotatedPart()
{
var drawing = MakeSquareWithHole();
var source = MakeLeadInPart(drawing, QuarterTurn);
var clone = (Part)source.Clone();
clone.RemoveLeadIns();
var expected = MakeCleanPart(drawing, QuarterTurn, source.Location);
Assert.False(clone.HasManualLeadIns);
Assert.Equal(QuarterTurn, clone.Rotation, 6);
AssertSameBox(expected.BoundingBox, clone.BoundingBox);
}
[Fact]
public void Clone_DoesNotShareHoleSubPrograms()
{
var drawing = MakeSquareWithHole();
var source = MakeLeadInPart(drawing, QuarterTurn);
var sourceBox = source.Program.BoundingBox();
var clone = (Part)source.Clone();
clone.Rotate(QuarterTurn);
// Rotating the copy must not rotate the source's hole lead-in.
AssertSameBox(sourceBox, source.Program.BoundingBox());
Assert.Equal(QuarterTurn, source.Rotation, 6);
}
[Fact]
public void Rotate_LeadInPart_ReportsCumulativeRotation()
{
var drawing = MakeSquareWithHole();
var part = MakeLeadInPart(drawing, QuarterTurn);
part.Rotate(QuarterTurn);
Assert.True(part.HasManualLeadIns);
Assert.Equal(System.Math.PI, part.Rotation, 6);
}
[Fact]
public void RotateAboutOrigin_LeadInPart_ReportsCumulativeRotation()
{
var drawing = MakeSquareWithHole();
var part = MakeLeadInPart(drawing, QuarterTurn);
part.Rotate(QuarterTurn, new Vector(50, 50));
Assert.Equal(System.Math.PI, part.Rotation, 6);
}
[Fact]
public void Rotate_LeadInPart_ThenRemove_RestoresCleanPartAtNewRotation()
{
var drawing = MakeSquareWithHole();
var part = MakeLeadInPart(drawing, QuarterTurn);
part.Rotate(QuarterTurn);
var location = part.Location;
part.RemoveLeadIns();
var expected = MakeCleanPart(drawing, System.Math.PI, location);
Assert.Equal(System.Math.PI, part.Rotation, 6);
AssertSameBox(expected.BoundingBox, part.BoundingBox);
}
[Fact]
public void Rotate_LeadInPart_RotatesHoleLeadInWithPart()
{
var drawing = MakeSquareWithHole();
var part = MakeLeadInPart(drawing, 0);
var before = part.Program.BoundingBox();
part.Rotate(QuarterTurn);
// A quarter turn about the program origin maps (x, y) to (-y, x): the lead-in
// program's box, holes included, must turn with it rather than stay behind.
var after = part.Program.BoundingBox();
Assert.Equal(-(before.Y + before.Width), after.X, 6);
Assert.Equal(before.X, after.Y, 6);
Assert.Equal(before.Width, after.Length, 6);
Assert.Equal(before.Length, after.Width, 6);
}
}