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();
|
EnsureOwnedProgram();
|
||||||
Program.Rotate(angle);
|
Program.Rotate(angle);
|
||||||
location = Location.Rotate(angle);
|
location = Location.Rotate(angle);
|
||||||
preLeadInRotation = Program.Rotation;
|
TrackRotation(angle);
|
||||||
UpdateBounds();
|
UpdateBounds();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,10 +159,22 @@ namespace OpenNest
|
|||||||
EnsureOwnedProgram();
|
EnsureOwnedProgram();
|
||||||
Program.Rotate(angle);
|
Program.Rotate(angle);
|
||||||
location = Location.Rotate(angle, origin);
|
location = Location.Rotate(angle, origin);
|
||||||
preLeadInRotation = Program.Rotation;
|
TrackRotation(angle);
|
||||||
UpdateBounds();
|
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>
|
/// <summary>
|
||||||
/// Offsets the part.
|
/// Offsets the part.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -323,6 +335,7 @@ namespace OpenNest
|
|||||||
new Box(BoundingBox.X, BoundingBox.Y, BoundingBox.Length, BoundingBox.Width)
|
new Box(BoundingBox.X, BoundingBox.Y, BoundingBox.Length, BoundingBox.Width)
|
||||||
);
|
);
|
||||||
part.ownsProgram = true;
|
part.ownsProgram = true;
|
||||||
|
part.CopyLeadInStateFrom(this);
|
||||||
|
|
||||||
return part;
|
return part;
|
||||||
}
|
}
|
||||||
@@ -347,10 +360,24 @@ namespace OpenNest
|
|||||||
BoundingBox.Width
|
BoundingBox.Width
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
part.CopyLeadInStateFrom(this);
|
||||||
|
|
||||||
return part;
|
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()
|
private void EnsureOwnedProgram()
|
||||||
{
|
{
|
||||||
if (!ownsProgram)
|
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