Files
OpenNest/OpenNest.Tests/CNC/ProgramSubProgramTransformTests.cs
T
aj a886735040 fix(cnc): keep hole sub-programs private to each program copy
Program.Clone deep-copied the SubPrograms dictionary but left every
SubProgramCall pointing at the source's sub-program, and
SubProgramCall.Clone went through the Rotation setter, which re-rotated
that shared program to the call's stale angle. Copying a program with
hole lead-ins therefore rotated the source's holes, and rotating the
copy rotated the source again.

Program.Rotate also rotated a shared sub-program once per call, so two
identical holes (one deduplicated sub-program) turned twice.

Clone now binds calls to one private copy per shared sub-program
without re-aligning it, and Rotate turns each distinct sub-program once.
2026-09-28 18:22:36 -04:00

112 lines
3.5 KiB
C#

using OpenNest.CNC;
using OpenNest.Converters;
using OpenNest.Geometry;
namespace OpenNest.Tests.CNC;
/// <summary>
/// Hole sub-programs are shared by reference: identical holes call one sub-program,
/// and program copies must not reach back into the source's sub-programs.
/// </summary>
public class ProgramSubProgramTransformTests
{
private const double QuarterTurn = System.Math.PI / 2;
/// <summary>Incremental hole sub-program: a lead-in from the hole centre to
/// (1, 0), then a full radius-1 circle.</summary>
private static Program MakeHoleSub()
{
var sub = new Program(Mode.Absolute);
sub.Codes.Add(new LinearMove(new Vector(1, 0)) { Layer = LayerType.Leadin });
sub.Codes.Add(new ArcMove(new Vector(1, 0), new Vector(0, 0), RotationType.CW));
sub.Mode = Mode.Incremental;
return sub;
}
/// <summary>Program with two identical holes, at (5, 5) and (15, 5), calling one
/// shared sub-program, as <c>ContourCuttingStrategy</c> emits them.</summary>
private static Program MakeTwoHoleProgram()
{
var sub = MakeHoleSub();
var pgm = new Program(Mode.Absolute);
pgm.SubPrograms[7] = sub;
pgm.Codes.Add(new SubProgramCall { Id = 7, Program = sub, Offset = new Vector(5, 5) });
pgm.Codes.Add(new SubProgramCall { Id = 7, Program = sub, Offset = new Vector(15, 5) });
return pgm;
}
/// <summary>End points of the lead-in lines in the program's own frame.</summary>
private static List<Vector> LeadInEnds(Program pgm) =>
ConvertProgram
.ToGeometry(pgm)
.OfType<Line>()
.Where(l => l.Layer == SpecialLayers.Leadin)
.Select(l => l.EndPoint)
.ToList();
private static void AssertPoint(double x, double y, Vector actual)
{
Assert.Equal(x, actual.X, 6);
Assert.Equal(y, actual.Y, 6);
}
[Fact]
public void Rotate_SharedSubProgram_TurnsEachHoleOnce()
{
var pgm = MakeTwoHoleProgram();
pgm.Rotate(QuarterTurn);
// (5, 5) -> (-5, 5) and (15, 5) -> (-5, 15); each lead-in ends one unit along
// the rotated +X axis, i.e. at +Y from its centre.
var ends = LeadInEnds(pgm);
Assert.Equal(2, ends.Count);
AssertPoint(-5, 6, ends[0]);
AssertPoint(-5, 16, ends[1]);
}
[Fact]
public void Clone_DoesNotRealignSourceSubProgram()
{
var pgm = MakeTwoHoleProgram();
pgm.Rotate(QuarterTurn);
var before = LeadInEnds(pgm);
_ = pgm.Clone();
var after = LeadInEnds(pgm);
AssertPoint(before[0].X, before[0].Y, after[0]);
AssertPoint(before[1].X, before[1].Y, after[1]);
}
[Fact]
public void Clone_OwnsItsSubPrograms()
{
var pgm = MakeTwoHoleProgram();
var copy = (Program)pgm.Clone();
copy.Rotate(QuarterTurn);
var sourceEnds = LeadInEnds(pgm);
AssertPoint(6, 5, sourceEnds[0]);
AssertPoint(16, 5, sourceEnds[1]);
var copyEnds = LeadInEnds(copy);
AssertPoint(-5, 6, copyEnds[0]);
AssertPoint(-5, 16, copyEnds[1]);
}
[Fact]
public void Clone_KeepsCallsAndDictionaryOnOneSharedCopy()
{
var pgm = MakeTwoHoleProgram();
var copy = (Program)pgm.Clone();
var calls = copy.Codes.OfType<SubProgramCall>().ToList();
Assert.NotSame(pgm.SubPrograms[7], copy.SubPrograms[7]);
Assert.Same(copy.SubPrograms[7], calls[0].Program);
Assert.Same(copy.SubPrograms[7], calls[1].Program);
}
}