using OpenNest.CNC;
using OpenNest.Converters;
using OpenNest.Geometry;
using OpenNest.IO;
namespace OpenNest.Tests.IO;
///
/// Nests converted from PEP carry etch marks only in each placed part's locked cutting program;
/// the drawing has none. Removing lead-ins rebuilds a part from its drawing, so loading such a
/// nest must give the marks back to the drawing or clearing lead-ins erases them.
///
public class ConvertedEtchMarkTests
{
private const double HalfTurn = System.Math.PI;
private const double QuarterTurn = System.Math.PI / 2;
[Theory]
[InlineData(Mode.Incremental)]
[InlineData(Mode.Absolute)]
public void ClearingLeadInsKeepsEtchMarksOfConvertedDrawing(Mode mode)
{
var loaded = Read(Save(ConvertedNest(mode)));
var parts = loaded.Plates[0].Parts;
AppliedCuttingWork.Clear(loaded.Plates.ToList());
Assert.All(parts, part => Assert.False(part.HasManualLeadIns));
AssertMarks(Expected(0, new Vector(0, 0)), parts[0]);
AssertMarks(Expected(QuarterTurn, new Vector(30, 2)), parts[1]);
AssertMarks(Expected(HalfTurn, new Vector(50, 10)), parts[2]);
}
///
/// Turning a mark is not symmetric, so a slanted placement proves the marks are put back in
/// the drawing's own frame rather than the frame of the placement they were read from.
///
[Theory]
[InlineData(Mode.Incremental)]
[InlineData(Mode.Absolute)]
public void MarksOfASlantedPlacementAreTurnedBackIntoTheDrawingFrame(Mode mode)
{
const double slant = System.Math.PI / 6;
var drawing = new Drawing("converted", Build(marks: false, mode));
var clean = new Part(drawing);
clean.Rotate(slant);
clean.Offset(12, 7);
var nest = NestOf(drawing, SavedPart(drawing, Build(marks: true, mode), slant, new Vector(12, 7)), clean);
var loaded = Read(Save(nest));
var parts = loaded.Plates[0].Parts;
Assert.Equal(2, MarkMoves(loaded.Drawings.Single().Program));
var inDrawingFrame = MarkSamples(loaded.Drawings.Single().Program, Vector.Zero);
Assert.Equal(MarkSamples(Build(marks: true, Mode.Incremental), Vector.Zero),
inDrawingFrame, VectorSame);
AssertMarks(Expected(slant, new Vector(12, 7)), parts[1]);
AppliedCuttingWork.Clear(loaded.Plates.ToList());
AssertMarks(Expected(slant, new Vector(12, 7)), parts[0]);
AssertMarks(Expected(slant, new Vector(12, 7)), parts[1]);
}
[Fact]
public void LoadingGivesTheMarksToCleanPartsAndLeavesSavedProgramsAlone()
{
var nest = ConvertedNest(Mode.Incremental);
var saved = nest.Plates[0].Parts.Take(2).Select(p => NestWriter.GetProgramText(p.Program)).ToArray();
var loaded = Read(Save(nest));
var parts = loaded.Plates[0].Parts;
Assert.Equal(2, MarkMoves(loaded.Drawings.Single().Program));
Assert.True(parts[0].LeadInsLocked);
Assert.Equal(saved[0], NestWriter.GetProgramText(parts[0].Program));
Assert.Equal(saved[1], NestWriter.GetProgramText(parts[1].Program));
Assert.False(parts[2].HasManualLeadIns);
AssertMarks(Expected(HalfTurn, new Vector(50, 10)), parts[2]);
}
[Fact]
public void RecoveredMarksAreSavedOnceAndKeepTheSavedPrograms()
{
var once = Read(Save(ConvertedNest(Mode.Incremental)));
var text = NestWriter.GetProgramText(once.Drawings.Single().Program);
var twice = Read(Save(once));
Assert.Equal(text, NestWriter.GetProgramText(twice.Drawings.Single().Program));
Assert.Equal(2, MarkMoves(twice.Drawings.Single().Program));
Assert.True(twice.Plates[0].Parts[0].HasManualLeadIns);
Assert.True(twice.Plates[0].Parts[1].HasManualLeadIns);
}
[Fact]
public void DrawingThatAlreadyHasMarksIsUnchanged()
{
var drawing = new Drawing("imported", Build(marks: true, Mode.Incremental));
var text = NestWriter.GetProgramText(drawing.Program);
var part = new Part(drawing);
part.Rotate(QuarterTurn);
part.Offset(30, 2);
part.ApplyLeadIns(new OpenNest.CNC.CuttingStrategy.CuttingParameters(), new Vector(-5, -5));
var nest = NestOf(drawing, part);
var loaded = Read(Save(nest));
AppliedCuttingWork.Clear(loaded.Plates.ToList());
Assert.Equal(text, NestWriter.GetProgramText(loaded.Drawings.Single().Program));
AssertMarks(Expected(QuarterTurn, new Vector(30, 2)), loaded.Plates[0].Parts[0]);
}
[Fact]
public void PartsWithDifferentMarksLeaveTheDrawingWithout()
{
var drawing = new Drawing("converted", Build(marks: false, Mode.Incremental));
var withMarks = SavedPart(drawing, Build(marks: true, Mode.Incremental), 0, new Vector(0, 0));
var other = Build(marks: true, Mode.Incremental);
other.Codes.RemoveAt(other.Codes.FindLastIndex(code => code is ArcMove));
var withLineOnly = SavedPart(drawing, other, 0, new Vector(20, 0));
var reader = new NestReader(new MemoryStream(Save(NestOf(drawing, withMarks, withLineOnly))));
var loaded = reader.Read();
Assert.Empty(reader.Warnings);
Assert.Equal(0, MarkMoves(loaded.Drawings.Single().Program));
Assert.True(loaded.Plates[0].Parts.All(p => p.HasManualLeadIns));
}
///
/// A converted nest: the drawing is material only, two placements keep the full PEP cutting
/// program (etch line and etch circle) at different turns, and one placement is clean.
///
private static Nest ConvertedNest(Mode mode)
{
var drawing = new Drawing("converted", Build(marks: false, mode));
var clean = new Part(drawing);
clean.Rotate(HalfTurn);
clean.Offset(50, 10);
return NestOf(drawing,
SavedPart(drawing, Build(marks: true, mode), 0, new Vector(0, 0)),
SavedPart(drawing, Build(marks: true, mode), QuarterTurn, new Vector(30, 2)),
clean);
}
private static Part SavedPart(Drawing drawing, Program cutting, double rotation, Vector location)
{
var part = new Part(drawing);
part.Rotate(rotation);
part.Offset(location);
var program = (Program)cutting.Clone();
program.Rotate(rotation);
Assert.True(part.RestoreLeadInProgram(program, locked: true));
return part;
}
/// A 10 x 6 plate with an etch line and an etch circle; without marks they are rapids.
private static Program Build(bool marks, Mode mode)
{
var program = new Program(Mode.Absolute);
program.Codes.Add(new RapidMove(2, 3));
program.Codes.Add(marks ? new LinearMove(4, 3) { Layer = LayerType.Scribe } : new RapidMove(4, 3));
program.Codes.Add(new RapidMove(7.5, 3));
program.Codes.Add(marks
? new ArcMove(new Vector(7.5, 3), new Vector(7, 3), RotationType.CCW) { Layer = LayerType.Scribe }
: new RapidMove(7.5, 3));
program.Codes.Add(new RapidMove(0, 0));
program.Codes.Add(new LinearMove(10, 0));
program.Codes.Add(new LinearMove(10, 6));
program.Codes.Add(new LinearMove(0, 6));
program.Codes.Add(new LinearMove(0, 0));
program.Mode = mode;
return program;
}
private static Nest NestOf(Drawing drawing, params Part[] parts)
{
var nest = new Nest { Name = "converted-etch" };
nest.Drawings.Add(drawing);
var plate = new Plate { Size = new Size(48, 96) };
foreach (var part in parts)
plate.Parts.Add(part);
nest.Plates.Add(plate);
return nest;
}
/// World-space mark samples of the full program turned and placed like a part.
private static List Expected(double rotation, Vector location)
{
var program = Build(marks: true, Mode.Incremental);
program.Rotate(rotation);
return MarkSamples(program, location);
}
private static void AssertMarks(List expected, Part part)
{
var actual = MarkSamples(part.Program, part.Location);
Assert.Equal(expected.Count, actual.Count);
foreach (var point in expected)
{
var index = actual.FindIndex(p => p.DistanceTo(point) <= 1e-4);
Assert.True(index >= 0, $"No etch mark at {point} on the part at {part.Location}.");
actual.RemoveAt(index);
}
}
// Turn-independent samples: line ends, and each circle's centre and radius.
private static List MarkSamples(Program program, Vector location) =>
ConvertProgram.ToGeometry(program)
.Where(entity => entity.Layer == SpecialLayers.Scribe)
.SelectMany(entity => entity switch
{
Line line => new[] { line.StartPoint + location, line.EndPoint + location },
Circle circle => new[] { circle.Center + location, new Vector(circle.Radius, 0) },
Arc arc => new[] { arc.StartPoint() + location, arc.EndPoint() + location },
_ => Array.Empty(),
})
.ToList();
private static bool VectorSame(Vector left, Vector right) => left.DistanceTo(right) <= 1e-6;
private static int MarkMoves(Program program) =>
program.Codes.Count(code => code is LinearMove { Layer: LayerType.Scribe } or ArcMove { Layer: LayerType.Scribe });
private static byte[] Save(Nest nest)
{
using var stream = new MemoryStream();
Assert.True(new NestWriter(nest).Write(stream));
return stream.ToArray();
}
private static Nest Read(byte[] bytes) => new NestReader(new MemoryStream(bytes)).Read();
}