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.
This commit is contained in:
+29
-2
@@ -145,7 +145,7 @@ namespace OpenNest
|
||||
EnsureOwnedProgram();
|
||||
Program.Rotate(angle);
|
||||
location = Location.Rotate(angle);
|
||||
preLeadInRotation = Program.Rotation;
|
||||
TrackRotation(angle);
|
||||
UpdateBounds();
|
||||
}
|
||||
|
||||
@@ -159,10 +159,22 @@ namespace OpenNest
|
||||
EnsureOwnedProgram();
|
||||
Program.Rotate(angle);
|
||||
location = Location.Rotate(angle, origin);
|
||||
preLeadInRotation = Program.Rotation;
|
||||
TrackRotation(angle);
|
||||
UpdateBounds();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records the part's rotation after it turned by <paramref name="angle"/>. A lead-in
|
||||
/// program is rebuilt by the cutting strategy and starts over at zero program
|
||||
/// rotation, so for those parts the rotation is accumulated rather than read back.
|
||||
/// </summary>
|
||||
private void TrackRotation(double angle)
|
||||
{
|
||||
preLeadInRotation = HasManualLeadIns
|
||||
? Angle.NormalizeRad(preLeadInRotation + angle)
|
||||
: Program.Rotation;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Offsets the part.
|
||||
/// </summary>
|
||||
@@ -323,6 +335,7 @@ namespace OpenNest
|
||||
new Box(BoundingBox.X, BoundingBox.Y, BoundingBox.Length, BoundingBox.Width)
|
||||
);
|
||||
part.ownsProgram = true;
|
||||
part.CopyLeadInStateFrom(this);
|
||||
|
||||
return part;
|
||||
}
|
||||
@@ -347,10 +360,24 @@ namespace OpenNest
|
||||
BoundingBox.Width
|
||||
)
|
||||
);
|
||||
part.CopyLeadInStateFrom(this);
|
||||
|
||||
return part;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies the lead-in state that goes with a copied program. Without it a copy of a
|
||||
/// lead-in part reads its rotation from the rebuilt program (zero), and lead-in
|
||||
/// assignment does not know to strip the copied lead-ins before adding new ones.
|
||||
/// </summary>
|
||||
private void CopyLeadInStateFrom(Part source)
|
||||
{
|
||||
HasManualLeadIns = source.HasManualLeadIns;
|
||||
LeadInsLocked = source.LeadInsLocked;
|
||||
CuttingParameters = source.CuttingParameters;
|
||||
preLeadInRotation = source.preLeadInRotation;
|
||||
}
|
||||
|
||||
private void EnsureOwnedProgram()
|
||||
{
|
||||
if (!ownsProgram)
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user