mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-13 23:55:08 -05:00
Part.CloneAtOffset hands the copy the source's Program instance. The copy cloned before rotating, but the source still owned the program, so rotating the source turned the program under every tiled copy while their location and bounds stayed put. Program now carries a sticky SharedByParts flag that CloneAtOffset sets, and a part copies its program before rotating when it does not own it or the program is shared. The flag lives on the program rather than the part so a restored cutting state, which reinstates ownership field for field, cannot re-alias a shared program. Unrotated copies keep sharing one instance, which the fill and overlap caches key on. Update, ApplyLeadIns and ApplySingleLeadIn install a fresh program and now own it, saving a needless copy on the next rotate. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
213 lines
7.0 KiB
C#
213 lines
7.0 KiB
C#
using OpenNest.CNC;
|
|
using OpenNest.CNC.CuttingPlanning;
|
|
using OpenNest.Geometry;
|
|
|
|
namespace OpenNest.Tests;
|
|
|
|
/// <summary>
|
|
/// <see cref="Part.CloneAtOffset"/> shares one program between the source and its copies. No
|
|
/// part may turn that program in place while another part still reads it.
|
|
/// </summary>
|
|
public class PartProgramSharingTests
|
|
{
|
|
private const double QuarterTurn = System.Math.PI / 2;
|
|
|
|
private static Part MakeSource()
|
|
{
|
|
// 4 x 1, so a quarter turn changes the bounds.
|
|
var pgm = new Program();
|
|
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
|
|
pgm.Codes.Add(new LinearMove(new Vector(0, 1)));
|
|
pgm.Codes.Add(new LinearMove(new Vector(4, 1)));
|
|
pgm.Codes.Add(new LinearMove(new Vector(4, 0)));
|
|
pgm.Codes.Add(new LinearMove(new Vector(0, 0)));
|
|
return new Part(new Drawing("test", pgm), new Vector(2, 3));
|
|
}
|
|
|
|
private static double[] Bounds(Part part) =>
|
|
[part.BoundingBox.X, part.BoundingBox.Y, part.BoundingBox.Length, part.BoundingBox.Width];
|
|
|
|
private static void AssertUntouched(Part part, Program expectedProgram, double expectedRotation,
|
|
double[] expectedBounds)
|
|
{
|
|
Assert.True(ProgramContent.Equal(part.Program, expectedProgram));
|
|
Assert.Equal(expectedRotation, part.Rotation);
|
|
Assert.Equal(expectedRotation, part.Program.Rotation);
|
|
Assert.Equal(expectedBounds, Bounds(part));
|
|
|
|
// The stored bounds must still be the bounds of the program the part holds.
|
|
var actual = part.Program.BoundingBox();
|
|
actual.Offset(part.Location);
|
|
Assert.Equal(expectedBounds[0], actual.X, 9);
|
|
Assert.Equal(expectedBounds[1], actual.Y, 9);
|
|
Assert.Equal(expectedBounds[2], actual.Length, 9);
|
|
Assert.Equal(expectedBounds[3], actual.Width, 9);
|
|
}
|
|
|
|
[Fact]
|
|
public void RotatingTheSource_LeavesTheCopyUnchanged()
|
|
{
|
|
var source = MakeSource();
|
|
var copy = source.CloneAtOffset(new Vector(10, 0));
|
|
var program = (Program)copy.Program.Clone();
|
|
var rotation = copy.Rotation;
|
|
var bounds = Bounds(copy);
|
|
|
|
source.Rotate(QuarterTurn);
|
|
|
|
AssertUntouched(copy, program, rotation, bounds);
|
|
Assert.NotSame(source.Program, copy.Program);
|
|
Assert.Equal(QuarterTurn, source.Rotation, 9);
|
|
}
|
|
|
|
[Fact]
|
|
public void RotatingTheSourceAroundAnOrigin_LeavesTheCopyUnchanged()
|
|
{
|
|
var source = MakeSource();
|
|
var copy = source.CloneAtOffset(new Vector(10, 0));
|
|
var program = (Program)copy.Program.Clone();
|
|
var rotation = copy.Rotation;
|
|
var bounds = Bounds(copy);
|
|
|
|
source.Rotate(QuarterTurn, new Vector(5, 5));
|
|
|
|
AssertUntouched(copy, program, rotation, bounds);
|
|
}
|
|
|
|
[Fact]
|
|
public void RotatingTheCopy_LeavesTheSourceUnchanged()
|
|
{
|
|
var source = MakeSource();
|
|
var copy = source.CloneAtOffset(new Vector(10, 0));
|
|
var program = (Program)source.Program.Clone();
|
|
var rotation = source.Rotation;
|
|
var bounds = Bounds(source);
|
|
|
|
copy.Rotate(QuarterTurn);
|
|
|
|
AssertUntouched(source, program, rotation, bounds);
|
|
Assert.NotSame(source.Program, copy.Program);
|
|
Assert.Equal(QuarterTurn, copy.Rotation, 9);
|
|
}
|
|
|
|
[Fact]
|
|
public void RotatingOneCopy_LeavesItsSiblingUnchanged()
|
|
{
|
|
var source = MakeSource();
|
|
var first = source.CloneAtOffset(new Vector(10, 0));
|
|
var second = first.CloneAtOffset(new Vector(10, 0));
|
|
var program = (Program)second.Program.Clone();
|
|
var rotation = second.Rotation;
|
|
var bounds = Bounds(second);
|
|
|
|
first.Rotate(QuarterTurn);
|
|
source.Rotate(-QuarterTurn);
|
|
|
|
AssertUntouched(second, program, rotation, bounds);
|
|
}
|
|
|
|
[Fact]
|
|
public void UnrotatedCopies_ShareTheSourceProgram()
|
|
{
|
|
var source = MakeSource();
|
|
var program = source.Program;
|
|
|
|
var first = source.CloneAtOffset(new Vector(10, 0));
|
|
var second = source.CloneAtOffset(new Vector(20, 0));
|
|
var third = first.CloneAtOffset(new Vector(0, 10));
|
|
first.Offset(1, 1);
|
|
|
|
Assert.Same(program, source.Program);
|
|
Assert.Same(program, first.Program);
|
|
Assert.Same(program, second.Program);
|
|
Assert.Same(program, third.Program);
|
|
Assert.True(program.SharedByParts);
|
|
}
|
|
|
|
[Fact]
|
|
public void CopiedProgram_StartsUnshared()
|
|
{
|
|
var source = MakeSource();
|
|
source.CloneAtOffset(new Vector(10, 0));
|
|
|
|
Assert.True(source.Program.SharedByParts);
|
|
Assert.False(((Program)source.Program.Clone()).SharedByParts);
|
|
Assert.False(source.Program.HeadCopy(2).SharedByParts);
|
|
Assert.False(((Part)source.Clone()).Program.SharedByParts);
|
|
|
|
// The part that turned gets a private program; turning it again copies nothing.
|
|
source.Rotate(QuarterTurn);
|
|
var program = source.Program;
|
|
Assert.False(program.SharedByParts);
|
|
source.Rotate(QuarterTurn);
|
|
Assert.Same(program, source.Program);
|
|
}
|
|
|
|
[Fact]
|
|
public void UnsharedPart_RotatesItsProgramInPlace()
|
|
{
|
|
var source = MakeSource();
|
|
var program = source.Program;
|
|
|
|
source.Rotate(QuarterTurn);
|
|
|
|
Assert.Same(program, source.Program);
|
|
}
|
|
|
|
[Fact]
|
|
public void RestoredCuttingState_RoundTripsAndStillProtectsTheCopy()
|
|
{
|
|
var source = MakeSource();
|
|
var state = source.CaptureCuttingState();
|
|
var sourceProgram = (Program)source.Program.Clone();
|
|
var sourceBounds = Bounds(source);
|
|
|
|
var copy = source.CloneAtOffset(new Vector(10, 0));
|
|
var copyProgram = (Program)copy.Program.Clone();
|
|
var copyRotation = copy.Rotation;
|
|
var copyBounds = Bounds(copy);
|
|
|
|
source.Rotate(QuarterTurn);
|
|
source.RestoreCuttingState(state);
|
|
|
|
// Field-exact: the same record comes back, references included.
|
|
Assert.Equal(state, source.CaptureCuttingState());
|
|
Assert.Same(state.Program, source.Program);
|
|
Assert.Same(state.BoundingBox, source.BoundingBox);
|
|
Assert.True(ProgramContent.Equal(source.Program, sourceProgram));
|
|
Assert.Equal(sourceBounds, Bounds(source));
|
|
|
|
// The restore re-aliased the copy's program with an owning part.
|
|
Assert.Same(source.Program, copy.Program);
|
|
|
|
source.Rotate(QuarterTurn);
|
|
|
|
AssertUntouched(copy, copyProgram, copyRotation, copyBounds);
|
|
Assert.NotSame(source.Program, copy.Program);
|
|
}
|
|
|
|
[Fact]
|
|
public void RotateAfterUpdate_KeepsTheProgramInstance()
|
|
{
|
|
var copy = MakeSource().CloneAtOffset(new Vector(10, 0));
|
|
|
|
copy.Update();
|
|
var program = copy.Program;
|
|
copy.Rotate(QuarterTurn);
|
|
|
|
Assert.Same(program, copy.Program);
|
|
}
|
|
|
|
[Fact]
|
|
public void RotateAfterApplyLeadIns_KeepsTheProgramInstance()
|
|
{
|
|
var copy = MakeSource().CloneAtOffset(new Vector(10, 0));
|
|
|
|
copy.ApplyLeadIns(new OpenNest.CNC.CuttingStrategy.CuttingParameters(), new Vector(-5, -5));
|
|
var program = copy.Program;
|
|
copy.Rotate(QuarterTurn);
|
|
|
|
Assert.Same(program, copy.Program);
|
|
}
|
|
}
|