using System.Globalization; using System.Security.Cryptography; using System.Text; using OpenNest.CNC; using OpenNest.CNC.CuttingPlanning; using OpenNest.CNC.CuttingStrategy; using OpenNest.Geometry; namespace OpenNest.Tests.CuttingPlanning; public class ExplicitContourTests { public static IEnumerable RetainedStyles() { foreach (var style in new[] { "line", "arc", "linearc", "lineline", "clean" }) foreach (var reversed in new[] { false, true }) foreach (var internalContour in new[] { false, true }) yield return [style, reversed, internalContour]; } [Theory] [MemberData(nameof(RetainedStyles))] public void LegacyEmission_RetainsStylesWindingAndCornerFallback(string style, bool reversed, bool internalContour) { var program = Square(reversed); var entities = program.ToGeometry().Where(e => e.Layer != SpecialLayers.Rapid).ToArray(); var parameters = Parameters(style); if (internalContour) { program.MoveTo(-5, -5); program.LineTo(-5, 15); program.LineTo(15, 15); program.LineTo(15, -5); program.LineTo(-5, -5); } var result = new ContourCuttingStrategy { Parameters = parameters }.ApplySingle(program, new Vector(0, 0), entities[0], internalContour ? ContourType.Internal : ContourType.External); var expected = $"{style}:{reversed}:{internalContour}" switch { "arc:False:False" => "4F89EDF317A545A0F496A1C7F9C633755663D98AFCF71F783B4AABF700D1711C", "arc:False:True" => "478018E0A86BA1FF0F08204EF5449D4484ADD1B58A40EA0936069DE6B6002E3F", "arc:True:False" => "CDC68828AD39D4B88C205347FDC06227C03A947F6AB3A19376F4BD670987C1A6", "arc:True:True" => "83CAD7BDCF4A655730D779966355F535E36429B5AAFA4423CB58E3B3FAAD4376", "clean:False:False" => "E3A6BE74CC4B72EC5DEFD9E53743DD080F6AF6854A0E7143860E5246745D1C12", "clean:False:True" => "196CC9BE57E9740AB8E35A77A48473DCD8B62312DE2903B2E66F62BB39818DDA", "clean:True:False" => "6E51376D40D93BD0DAD2E51FFAC732A9BE1717C252964BA2285A8440A23C3480", "clean:True:True" => "80DB265B4E9846169421B3DDCB8DAE003D720EEE8EF9DAB64C40EE00CC322DE6", "line:False:False" => "A9DAB8F802CDE0A89A0D26B4C5AC5546E121E71AF9EBA2283F307ADF88866347", "line:False:True" => "61EE8CF9A30B18C72DDACD314886709692CA8E50E7DC61170A900429562A45B5", "line:True:False" => "AB6E00410C3AB8592B9DD7C5EC1E9E8ABD39A2DCA9486FCEAE28593D9BA7504C", "line:True:True" => "23E2404046438ECF67BAC46AC7692B6DF60EFA674C286D72B25F98FE77431588", "linearc:False:False" => "E3A6BE74CC4B72EC5DEFD9E53743DD080F6AF6854A0E7143860E5246745D1C12", "linearc:False:True" => "196CC9BE57E9740AB8E35A77A48473DCD8B62312DE2903B2E66F62BB39818DDA", "linearc:True:False" => "6E51376D40D93BD0DAD2E51FFAC732A9BE1717C252964BA2285A8440A23C3480", "linearc:True:True" => "80DB265B4E9846169421B3DDCB8DAE003D720EEE8EF9DAB64C40EE00CC322DE6", "lineline:False:False" => "F25E4FA39713A53D04FC5DA22BD3FD1EB15330462153356906AD4DAA1C1A23C2", "lineline:False:True" => "911C85F1C6A053F9C94F63A40B520C30B00AA43B804634E0203DD406BB98BF6E", "lineline:True:False" => "AE8E6CD1B641D33A39DAE07A6FAF539668BD794DC8105F4D41C1BE33927FFC65", "lineline:True:True" => "54E1F71B8420ACAF1C57E124A0F927BA00F04F52CDBAB48EDFC27BAB8C346EA5", _ => throw new InvalidOperationException() }; Assert.Equal(expected, Fingerprint(result.Program)); } [Fact] public void LegacyCircle_RoundsActualEntryClampsAndSharesSubprograms() { var program = Square(false); foreach (var center in new[] { new Vector(3, 3), new Vector(7, 7) }) { program.MoveTo(center.X + 0.5, center.Y); program.ArcTo(center.X + 0.5, center.Y, center.X, center.Y, RotationType.CCW); } var parameters = Parameters("line"); parameters.RoundLeadInAngles = true; parameters.LeadInAngleIncrement = 90; parameters.ArcCircleLeadIn = new LineLeadIn { Length = 2 }; var result = new ContourCuttingStrategy { Parameters = parameters }.Apply(program, new Vector(20, 20)); var calls = result.Program.Codes.OfType().ToArray(); Assert.Equal(2, calls.Length); Assert.Same(calls[0].Program, calls[1].Program); var moves = ExecutionMotionReader.Read(result.Program, Vector.Zero, null, default).Motions; Assert.All(moves.Where(m => m.Layer == LayerType.Leadin).Take(2), m => Assert.True(m.Length < 1)); Assert.Equal("9BB777CDD1F7407EB0D4B0FE1D1A760370A094A606E61B7E3875DCB5EF051F20", Fingerprint(result.Program)); } [Fact] public void LegacyTab_LeavesGenuineGap() { var parameters = Parameters("line"); parameters.TabsEnabled = true; parameters.TabConfig = new NormalTab { Size = 0.2 }; var result = new ContourCuttingStrategy { Parameters = parameters }.Apply(Square(false), new Vector(-1, 5)); var moves = ExecutionMotionReader.Read(result.Program, Vector.Zero, null, default).Motions; var cuts = moves.Where(m => !m.Rapid && m.Layer == LayerType.Display).ToArray(); Assert.True(cuts[0].Start!.Value.DistanceTo(cuts[^1].End) > 0.1); Assert.Equal("C5CDFACDE4019AEBFCD4AA304D149DD603313D341311F27C9ECBB4ED940D2204", Fingerprint(result.Program)); } internal static Program Square(bool reversed) { var p = new Program(); p.MoveTo(0, 0); if (reversed) { p.LineTo(10, 0); p.LineTo(10, 10); p.LineTo(0, 10); } else { p.LineTo(0, 10); p.LineTo(10, 10); p.LineTo(10, 0); } p.LineTo(0, 0); return p; } internal static CuttingParameters Parameters(string style = "line") { LeadIn lead = style switch { "arc" => new ArcLeadIn { Radius = 0.2 }, "linearc" => new LineArcLeadIn { LineLength = 0.1, ArcRadius = 0.2 }, "lineline" => new LineLineLeadIn { Length1 = 0.1, Length2 = 0.2 }, "clean" => new CleanHoleLeadIn { LineLength = 0.1, ArcRadius = 0.2, Kerf = 0.01 }, _ => new LineLeadIn { Length = 0.3, ApproachAngle = 45 } }; return new CuttingParameters { ExternalLeadIn = lead, InternalLeadIn = lead, ArcCircleLeadIn = lead, ExternalLeadOut = new NoLeadOut(), InternalLeadOut = new NoLeadOut(), PierceClearance = 0.05 }; } internal static string Fingerprint(Program program) { var execution = ExecutionMotionReader.Read(program, Vector.Zero, null, default); var text = string.Join("\n", execution.Motions.Select(m => string.Join("|", m.Rapid, m.Layer, m.Start?.X.ToString("R", CultureInfo.InvariantCulture), m.Start?.Y.ToString("R", CultureInfo.InvariantCulture), m.End.X.ToString("R", CultureInfo.InvariantCulture), m.End.Y.ToString("R", CultureInfo.InvariantCulture), m.Length.ToString("R", CultureInfo.InvariantCulture)))); return Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(text))); } }