mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-05 06:51:34 -05:00
Full circles now take InternalLeadIn/InternalLeadOut like every other cutout; the ArcCircle lead settings and the cutting panel's Arc / Circle tab are removed. Circle-specific geometry handling (radius clamp, angle rounding, pierce zone) is unchanged. Older nest files and desktop settings still load: their arcCircleLeadIn/ arcCircleLeadOut keys are ignored and no longer written, so re-planned circles take the internal leads. Saved part programs are untouched. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
672 lines
26 KiB
C#
672 lines
26 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
using OpenNest.CNC;
|
|
using OpenNest.CNC.CuttingStrategy;
|
|
using OpenNest.Engine;
|
|
using OpenNest.Engine.Sequencing;
|
|
using OpenNest.Geometry;
|
|
using OpenNest.Posts.CincinnatiCIFiber;
|
|
|
|
namespace OpenNest.Tests.CincinnatiCIFiber;
|
|
|
|
/// <summary>
|
|
/// Output-contract tests for the Cincinnati CI Fiber post, checked against the
|
|
/// structure of a Cincinnati machine sample NC: header block,
|
|
/// restart jump, N-labelled contour blocks with /L macro lines, G41
|
|
/// selection, trimmed coordinate format and the M50/M30/% tail.
|
|
/// </summary>
|
|
public class CIFiberPostProcessorTests
|
|
{
|
|
private static CIFiberPostConfig MakeConfig() =>
|
|
new() { ConfigurationName = "CI FIBER 8K" };
|
|
|
|
private static string Post(Nest nest, CIFiberPostConfig? config = null)
|
|
{
|
|
var post = new CIFiberPostProcessor(config ?? MakeConfig());
|
|
using var ms = new MemoryStream();
|
|
post.Post(nest, ms);
|
|
return Encoding.UTF8.GetString(ms.ToArray());
|
|
}
|
|
|
|
/// <summary>
|
|
/// 10x10 CW square perimeter (closed loop, no leading rapid) at the plate
|
|
/// location, with one CCW full-circle hole posted as an incremental
|
|
/// SubProgramCall at (5,5) — the exact shape OpenNest nests produce.
|
|
/// </summary>
|
|
private static Nest MakeSquareWithHoleNest(Vector? partLocation = null)
|
|
{
|
|
var location = partLocation ?? new Vector(1.0, 2.0);
|
|
|
|
var pgm = new Program(Mode.Absolute);
|
|
|
|
// Hole sub-program: CCW full circle r=0.5 centred on the call offset,
|
|
// linear lead-in from below, left absolute then flipped to
|
|
// incremental — exactly what ContourCuttingStrategy emits for a hole.
|
|
var hole = new Program(Mode.Absolute);
|
|
hole.Codes.Add(new RapidMove(new Vector(0, -0.6)));
|
|
hole.Codes.Add(new LinearMove(new Vector(0, -0.5)) { Layer = LayerType.Leadin });
|
|
hole.Codes.Add(
|
|
new ArcMove(new Vector(0, -0.5), new Vector(0, 0), RotationType.CCW)
|
|
{
|
|
Layer = LayerType.Cut,
|
|
}
|
|
);
|
|
hole.Mode = Mode.Incremental;
|
|
pgm.Codes.Add(new SubProgramCall { Id = 1, Program = hole, Offset = new Vector(5, 5) });
|
|
|
|
// Perimeter: linear lead-in into a CW loop starting at (0,0).
|
|
pgm.Codes.Add(new RapidMove(new Vector(0, -0.5)));
|
|
pgm.Codes.Add(new LinearMove(new Vector(0, 0)) { Layer = LayerType.Leadin });
|
|
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)));
|
|
|
|
var drawing = new Drawing("square-hole", pgm);
|
|
var part = new Part(drawing, location);
|
|
|
|
var plate = new Plate(60, 120);
|
|
plate.Parts.Add(part);
|
|
|
|
var nest = new Nest("Test Nest") { Thickness = 0.06, Material = new Material("Mild Steel") };
|
|
nest.Plates.Add(plate);
|
|
return nest;
|
|
}
|
|
|
|
private static List<string> Lines(string output) =>
|
|
output.Replace("\r\n", "\n").Split('\n').ToList();
|
|
|
|
[Fact]
|
|
public void Post_Structure_GoldenSquareWithHole()
|
|
{
|
|
var output = Post(MakeSquareWithHoleNest());
|
|
var lines = Lines(output);
|
|
|
|
// File ends with % and uses CRLF throughout, no BOM.
|
|
Assert.StartsWith("(", output);
|
|
Assert.EndsWith("%\r\n", output);
|
|
foreach (var raw in output.Split('\n').SkipLast(1))
|
|
Assert.EndsWith("\r", raw);
|
|
|
|
// Header block
|
|
Assert.Equal("( Test Nest )", lines[0]);
|
|
Assert.Equal("( CONFIGURATION - CI FIBER 8K )", lines[1]);
|
|
Assert.Matches(@"^\( \w+ \d+, \d{4} \d\d:\d\d (AM|PM) \)$", lines[2]);
|
|
Assert.Equal("( Material = Mild Steel .060 )", lines[3]);
|
|
Assert.Equal("V.E.MATERIAL = \"MSN\"", lines[4]);
|
|
Assert.Equal("V.E.THICKNESS = 0.060", lines[5]);
|
|
Assert.Equal("V.E.X_SIZE = 120.000", lines[6]);
|
|
Assert.Equal("V.E.Y_SIZE = 60.000", lines[7]);
|
|
Assert.Equal("V.E.UNIT = 1", lines[8]);
|
|
Assert.DoesNotContain("V.E.SHEET_WEIGHT", output);
|
|
|
|
// Restart preamble
|
|
Assert.Equal("G90", lines[9]);
|
|
Assert.Equal("L PROGRAMSTART.NC", lines[10]);
|
|
Assert.Equal("P3=V.E.R3", lines[11]);
|
|
Assert.Equal("$GOTO NP3:", lines[12]);
|
|
Assert.Equal("N0:", lines[13]);
|
|
Assert.Equal("( Sheet number - 1 )", lines[14]);
|
|
|
|
// Part block
|
|
Assert.Equal("( Part #1 )", lines[15]);
|
|
Assert.Equal("( PART:square-hole )", lines[16]);
|
|
Assert.Equal("V.E.R4=1", lines[17]);
|
|
|
|
// Contour 1 = the flattened hole (part program order).
|
|
Assert.Equal("N1:", lines[18]);
|
|
Assert.Equal("/L \"L0\"", lines[19]);
|
|
Assert.Equal("V.E.R3=1", lines[20]);
|
|
Assert.Equal("G0X6Y6.4", lines[21]); // pierce = part(1,2) + hole(5,5) + (0,-0.6)
|
|
Assert.Equal("/L \"L2\"", lines[22]);
|
|
Assert.Equal("G41", lines[23]);
|
|
Assert.Equal("G1X6Y6.5", lines[24]); // contour start = hole centre + (0,-0.5)
|
|
Assert.Equal("/L \"L6\"", lines[25]);
|
|
Assert.Equal("G3X6Y6.5I0J0.5", lines[26]); // I/J incremental from arc start
|
|
Assert.Equal("/L \"ZHSOFF\"", lines[27]);
|
|
|
|
// Contour 2 = perimeter.
|
|
Assert.Equal("N2:", lines[28]);
|
|
Assert.Equal("/L \"L0\"", lines[29]);
|
|
Assert.Equal("V.E.R3=2", lines[30]);
|
|
Assert.Equal("G0X1Y1.5", lines[31]);
|
|
Assert.Equal("/L \"L4\"", lines[32]);
|
|
// CW perimeter: the part is right of travel, so the kerf goes left.
|
|
Assert.Equal("G41", lines[33]);
|
|
Assert.Equal("G1X1Y2", lines[34]);
|
|
Assert.Equal("/L \"L6\"", lines[35]);
|
|
Assert.Equal("G1X1Y12", lines[36]);
|
|
Assert.Equal("G1X11Y12", lines[37]);
|
|
Assert.Equal("G1X11Y2", lines[38]);
|
|
Assert.Equal("G1X1Y2", lines[39]);
|
|
Assert.Equal("/L \"ZHSOFF\"", lines[40]);
|
|
|
|
// Part end and tail
|
|
Assert.Equal("( PART END )", lines[41]);
|
|
Assert.Equal("/L \"L0\"", lines[42]);
|
|
Assert.Equal("L PROGRAMEND.NC", lines[43]);
|
|
Assert.Equal("M50", lines[44]);
|
|
Assert.Equal("M30", lines[45]);
|
|
Assert.Equal("%", lines[46]);
|
|
}
|
|
|
|
/// <summary>
|
|
/// End to end through the real cutting strategy, with a drawing wound
|
|
/// against OpenNest's convention (perimeter CCW, hole CW, as a drawing built
|
|
/// in code may be): the compensation must still keep the product on the side
|
|
/// the controller leaves it (G41 right of travel, G42 left), for the
|
|
/// perimeter and the hole alike, and each must get its own lead-in macro.
|
|
/// The wrong side under- or oversizes the cut by a kerf width.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Post_Compensation_KeepsPartOnProductSide_ForPerimeterAndHole()
|
|
{
|
|
var pgm = new Program(Mode.Absolute);
|
|
// Square drawn CCW, the opposite of OpenNest's CW perimeter convention.
|
|
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
|
|
pgm.Codes.Add(new LinearMove(new Vector(10, 0)));
|
|
pgm.Codes.Add(new LinearMove(new Vector(10, 10)));
|
|
pgm.Codes.Add(new LinearMove(new Vector(0, 10)));
|
|
pgm.Codes.Add(new LinearMove(new Vector(0, 0)));
|
|
// Hole r=1 at (5,5), drawn CW, the opposite of OpenNest's CCW hole convention.
|
|
pgm.Codes.Add(new RapidMove(new Vector(6, 5)));
|
|
pgm.Codes.Add(new ArcMove(new Vector(6, 5), new Vector(5, 5), RotationType.CW));
|
|
|
|
var location = new Vector(3, 4);
|
|
var plate = new Plate(60, 120);
|
|
plate.Parts.Add(new Part(new Drawing("rect-hole", pgm), location));
|
|
plate.CuttingParameters = new CuttingParameters
|
|
{
|
|
InternalLeadIn = new LineLeadIn { Length = 0.125, ApproachAngle = 90 },
|
|
ExternalLeadIn = new LineLeadIn { Length = 0.125, ApproachAngle = 90 },
|
|
};
|
|
TestLeadIns.Apply(plate);
|
|
|
|
var nest = new Nest("kerf") { Thickness = 0.06, Material = new Material("Mild Steel") };
|
|
nest.Plates.Add(plate);
|
|
var lines = Lines(Post(nest));
|
|
|
|
var holeCenter = location + new Vector(5, 5);
|
|
bool InMaterial(Vector p) =>
|
|
p.X > location.X && p.X < location.X + 10
|
|
&& p.Y > location.Y && p.Y < location.Y + 10
|
|
&& p.DistanceTo(holeCenter) > 1.0;
|
|
|
|
var checkedContours = 0;
|
|
for (var i = 0; i < lines.Count; i++)
|
|
{
|
|
if (lines[i] != "G41" && lines[i] != "G42")
|
|
continue;
|
|
|
|
// The lead-in ends on the contour; the first cut after /L "L6" sets
|
|
// the direction of travel there.
|
|
var start = ParseXY(lines[i + 1]);
|
|
var onPerimeter = start.DistanceTo(holeCenter) > 1.5;
|
|
Assert.Equal(onPerimeter ? "/L \"L4\"" : "/L \"L2\"", lines[i - 1]);
|
|
Assert.Equal("/L \"L6\"", lines[i + 2]);
|
|
var cut = lines[i + 3];
|
|
Vector travel;
|
|
if (cut.StartsWith("G1"))
|
|
{
|
|
travel = ParseXY(cut) - start;
|
|
}
|
|
else
|
|
{
|
|
var m = Regex.Match(cut, @"I(-?[\d.]+)J(-?[\d.]+)");
|
|
var radial = new Vector(
|
|
-double.Parse(m.Groups[1].Value, CultureInfo.InvariantCulture),
|
|
-double.Parse(m.Groups[2].Value, CultureInfo.InvariantCulture)
|
|
); // centre -> start
|
|
travel = cut.StartsWith("G3")
|
|
? new Vector(-radial.Y, radial.X)
|
|
: new Vector(radial.Y, -radial.X);
|
|
}
|
|
|
|
var length = System.Math.Sqrt(travel.X * travel.X + travel.Y * travel.Y);
|
|
var right = new Vector(travel.Y / length, -travel.X / length);
|
|
if (lines[i] == "G42")
|
|
right = new Vector(-right.X, -right.Y); // G42 keeps the product on the left
|
|
Assert.True(InMaterial(start + right * 0.01), $"Part is not right of travel at {lines[i + 1]}");
|
|
Assert.False(InMaterial(start - right * 0.01), $"Kerf side is not scrap at {lines[i + 1]}");
|
|
checkedContours++;
|
|
}
|
|
|
|
Assert.Equal(2, checkedContours);
|
|
}
|
|
|
|
private static Vector ParseXY(string line)
|
|
{
|
|
var m = Regex.Match(line, @"X(-?[\d.]+)Y(-?[\d.]+)");
|
|
Assert.True(m.Success, line);
|
|
return new Vector(
|
|
double.Parse(m.Groups[1].Value, CultureInfo.InvariantCulture),
|
|
double.Parse(m.Groups[2].Value, CultureInfo.InvariantCulture)
|
|
);
|
|
}
|
|
|
|
[Fact]
|
|
public void Post_FlattensRotatedPartHole_ToSheetCoords()
|
|
{
|
|
// Same nest, but the part is rotated 90 degrees CCW about the origin:
|
|
// (x,y) -> (-y,x). Location (1,2) -> (-2,1); hole offset (5,5) -> (-5,5).
|
|
var nest = MakeSquareWithHoleNest();
|
|
var part = nest.Plates[0].Parts[0];
|
|
part.Rotate(OpenNest.Math.Angle.ToRadians(90));
|
|
|
|
var output = Post(nest);
|
|
var lines = Lines(output);
|
|
|
|
// Hole pierce local (0,-0.6) -> (0.6,0): (-2,1)+(-5,5)+(0.6,0) = (-6.4,6).
|
|
Assert.Equal("G0X-6.4Y6", lines[21]);
|
|
// Contour start local (0,-0.5) -> (0.5,0) = (-6.5,6).
|
|
Assert.Equal("G1X-6.5Y6", lines[24]);
|
|
// Full CCW circle, centre local (0,0.5) from the start -> (-0.5,0):
|
|
// centre at (-7,6), so I=-0.5 J=0.
|
|
Assert.Equal("G3X-6.5Y6I-0.5J0", lines[26]);
|
|
// Perimeter corner local (0,10) -> (-10,0): pierce (-2,1)+(0.5,0)...
|
|
// pierce local (0,-0.5)->(0.5,0) = (-1.5,1); corner (-10,1)... check
|
|
// first cut G1 target = lead-in end = (-2,1).
|
|
Assert.Equal("G0X-1.5Y1", lines[31]);
|
|
Assert.Equal("G1X-2Y1", lines[34]);
|
|
Assert.Equal("G1X-12Y1", lines[36]);
|
|
}
|
|
|
|
[Fact]
|
|
public void Post_CoordinateFormat_TrimsZerosAndNeverNegativeZero()
|
|
{
|
|
// Part placed so a coordinate lands on -0.0004 (rounds to -0) and on
|
|
// values whose 3-decimal representation keeps trailing digits.
|
|
var pgm = new Program(Mode.Absolute);
|
|
pgm.Codes.Add(new RapidMove(new Vector(-0.0004, 3.5009)));
|
|
pgm.Codes.Add(new LinearMove(new Vector(5, 3.5009)) { Layer = LayerType.Leadin });
|
|
pgm.Codes.Add(new LinearMove(new Vector(5, 4)) { Layer = LayerType.Cut });
|
|
pgm.Codes.Add(new LinearMove(new Vector(-0.0004, 4)) { Layer = LayerType.Cut });
|
|
pgm.Codes.Add(new LinearMove(new Vector(-0.0004, 3.5009)) { Layer = LayerType.Cut });
|
|
|
|
var drawing = new Drawing("fmt", pgm);
|
|
var plate = new Plate(60, 120);
|
|
plate.Parts.Add(new Part(drawing, Vector.Zero));
|
|
var nest = new Nest("fmt") { Thickness = 0.06, Material = new Material("Mild Steel") };
|
|
nest.Plates.Add(plate);
|
|
|
|
var output = Post(nest);
|
|
|
|
Assert.Contains("G0X0Y3.501", output); // -0.0004 -> "0" not "-0"; 3.5009 -> 3.501
|
|
Assert.Contains("G1X5Y3.501", output);
|
|
Assert.DoesNotContain("-0Y", output);
|
|
Assert.DoesNotContain("-0\r", output);
|
|
}
|
|
|
|
[Fact]
|
|
public void Post_RejectsArcLeadIn_PerTf5200LinearFirstRule()
|
|
{
|
|
// A contour whose first move after the comp selection is an arc must
|
|
// be rejected (TF5200 13.2.4.1: first motion block after selection
|
|
// must be LINEAR).
|
|
var pgm = new Program(Mode.Absolute);
|
|
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
|
|
pgm.Codes.Add(new ArcMove(0, 0, 0, 0.5, RotationType.CCW) { Layer = LayerType.Cut });
|
|
|
|
var drawing = new Drawing("arcdirect", pgm);
|
|
var plate = new Plate(60, 120);
|
|
plate.Parts.Add(new Part(drawing, Vector.Zero));
|
|
var nest = new Nest("arc") { Thickness = 0.06, Material = new Material("Mild Steel") };
|
|
nest.Plates.Add(plate);
|
|
|
|
var ex = Assert.Throws<InvalidOperationException>(() => Post(nest));
|
|
Assert.Contains("13.2.4.1", ex.Message);
|
|
}
|
|
|
|
[Fact]
|
|
public void Post_CutOff_PostsWithoutCompensationOrLeadIn()
|
|
{
|
|
var nest = MakeSquareWithHoleNest();
|
|
var plate = nest.Plates[0];
|
|
|
|
// Vertical cut-off at X=20: a bare rapid + line, no lead-in.
|
|
var cutOff = new CutOff(new Vector(20, 0), CutOffAxis.Vertical);
|
|
cutOff.Regenerate(plate, new CutOffSettings());
|
|
plate.Parts.Add(new Part(cutOff.Drawing, Vector.Zero));
|
|
|
|
var lines = Lines(Post(nest));
|
|
|
|
var n3 = lines.IndexOf("N3:");
|
|
Assert.True(n3 > lines.IndexOf("( PART:CutOff-V-20.00 )"));
|
|
Assert.Equal("/L \"L0\"", lines[n3 + 1]);
|
|
Assert.Equal("V.E.R3=3", lines[n3 + 2]);
|
|
Assert.StartsWith("G0X20Y", lines[n3 + 3]);
|
|
Assert.Equal("/L \"L4\"", lines[n3 + 4]);
|
|
Assert.Equal("/L \"L6\"", lines[n3 + 5]);
|
|
Assert.StartsWith("G1X20Y", lines[n3 + 6]);
|
|
Assert.Equal("/L \"ZHSOFF\"", lines[n3 + 7]);
|
|
}
|
|
|
|
[Fact]
|
|
public void Post_CutOff_FollowsPlateSequence()
|
|
{
|
|
// Plate order is the cut sequence: part, cut-off, part. The cut-off is
|
|
// sequence 2, so it posts second, not after every part.
|
|
var nest = MakeSquareWithHoleNest();
|
|
var plate = nest.Plates[0];
|
|
plate.Parts.Add(new Part(plate.Parts[0].BaseDrawing, new Vector(40, 2)));
|
|
|
|
var cutOff = new CutOff(new Vector(20, 0), CutOffAxis.Vertical);
|
|
cutOff.Regenerate(plate, new CutOffSettings());
|
|
plate.Parts.Insert(1, new Part(cutOff.Drawing, Vector.Zero));
|
|
|
|
var lines = Lines(Post(nest));
|
|
|
|
Assert.Equal(
|
|
new[] { "( PART:square-hole )", "( PART:CutOff-V-20.00 )", "( PART:square-hole )" },
|
|
lines.Where(l => l.StartsWith("( PART:")).ToArray()
|
|
);
|
|
|
|
// Part number and restart label run in the same order: the first
|
|
// part's hole and perimeter are N1/N2, so the cut-off is N3.
|
|
var cutOffBlock = lines.IndexOf("( PART:CutOff-V-20.00 )");
|
|
Assert.Equal("( Part #2 )", lines[cutOffBlock - 1]);
|
|
Assert.Equal("V.E.R4=2", lines[cutOffBlock + 1]);
|
|
Assert.Equal("N3:", lines[cutOffBlock + 2]);
|
|
}
|
|
|
|
[Fact]
|
|
public void Post_ValidatesTableSize()
|
|
{
|
|
var nest = MakeSquareWithHoleNest();
|
|
|
|
var tight = MakeConfig();
|
|
tight.MaxTableX = 100; // plate length is 120
|
|
Assert.Contains("exceeds maximum table X", Assert.Throws<InvalidOperationException>(() => Post(nest, tight)).Message);
|
|
|
|
var wide = MakeConfig();
|
|
wide.MaxTableX = 160.25;
|
|
wide.MaxTableY = 81.25;
|
|
Post(nest, wide); // does not throw
|
|
}
|
|
|
|
[Fact]
|
|
public void Post_SkipsSuppressedAndScribeMoves()
|
|
{
|
|
var pgm = new Program(Mode.Absolute);
|
|
|
|
// Suppressed contour (every move suppressed, as suppression marks a
|
|
// whole feature) must not appear.
|
|
pgm.Codes.Add(new RapidMove(new Vector(30, 30)) { Suppressed = true });
|
|
pgm.Codes.Add(
|
|
new LinearMove(new Vector(31, 30)) { Layer = LayerType.Leadin, Suppressed = true }
|
|
);
|
|
pgm.Codes.Add(
|
|
new LinearMove(new Vector(31, 31)) { Layer = LayerType.Cut, Suppressed = true }
|
|
);
|
|
|
|
// Scribe contour skipped by default.
|
|
pgm.Codes.Add(new RapidMove(new Vector(40, 40)));
|
|
pgm.Codes.Add(new LinearMove(new Vector(41, 40)) { Layer = LayerType.Leadin });
|
|
pgm.Codes.Add(new LinearMove(new Vector(41, 41)) { Layer = LayerType.Scribe });
|
|
|
|
// Cutting contour that must appear.
|
|
pgm.Codes.Add(new RapidMove(new Vector(50, 50)));
|
|
pgm.Codes.Add(new LinearMove(new Vector(51, 50)) { Layer = LayerType.Leadin });
|
|
pgm.Codes.Add(new LinearMove(new Vector(51, 51)) { Layer = LayerType.Cut });
|
|
pgm.Codes.Add(new LinearMove(new Vector(50, 51)) { Layer = LayerType.Cut });
|
|
pgm.Codes.Add(new LinearMove(new Vector(50, 50)) { Layer = LayerType.Cut });
|
|
|
|
var drawing = new Drawing("layers", pgm);
|
|
var plate = new Plate(60, 120);
|
|
plate.Parts.Add(new Part(drawing, Vector.Zero));
|
|
var nest = new Nest("layers") { Thickness = 0.06, Material = new Material("Mild Steel") };
|
|
nest.Plates.Add(plate);
|
|
|
|
var output = Post(nest);
|
|
var lines = Lines(output);
|
|
|
|
// Exactly one contour survived (N0: is the restart-jump label).
|
|
Assert.Single(lines.Where(l => l.StartsWith("N") && l != "N0:" && l.EndsWith(":")));
|
|
Assert.DoesNotContain("X30Y30", output);
|
|
Assert.DoesNotContain("X40Y40", output);
|
|
Assert.Contains("G0X50Y50", output);
|
|
Assert.Contains("V.E.R3=1", output);
|
|
}
|
|
|
|
[Fact]
|
|
public void MaterialCodes_MatchIgnoringCase_AfterJsonReload()
|
|
{
|
|
var options = new System.Text.Json.JsonSerializerOptions();
|
|
var json = System.Text.Json.JsonSerializer.Serialize(
|
|
new CIFiberPostConfig
|
|
{
|
|
MaterialCodes = new() { ["Stainless"] = "SSN" },
|
|
DefaultMaterialCode = "MSN",
|
|
},
|
|
options
|
|
);
|
|
|
|
var config = System.Text.Json.JsonSerializer.Deserialize<CIFiberPostConfig>(json, options)!;
|
|
|
|
Assert.Equal("SSN", config.ResolveMaterialCode("stainless"));
|
|
Assert.Equal("SSN", config.ResolveMaterialCode(" STAINLESS "));
|
|
Assert.Equal("MSN", config.ResolveMaterialCode("Aluminum"));
|
|
}
|
|
|
|
[Fact]
|
|
public void SavedConfigWithRetiredPartComment_StillLoads()
|
|
{
|
|
const string json = """
|
|
{ "PartComment": "OLD", "MaxTableX": 120.0, "DefaultMaterialCode": "SSN" }
|
|
""";
|
|
|
|
var config = System.Text.Json.JsonSerializer.Deserialize<CIFiberPostConfig>(json)!;
|
|
|
|
Assert.Equal(120.0, config.MaxTableX);
|
|
Assert.Equal("SSN", config.DefaultMaterialCode);
|
|
}
|
|
|
|
private static Nest MakeMultiSheetNest(params (double Width, double Length)[] extraSheets)
|
|
{
|
|
var nest = MakeSquareWithHoleNest();
|
|
var drawing = nest.Plates[0].Parts[0].BaseDrawing;
|
|
foreach (var (width, length) in extraSheets)
|
|
{
|
|
var plate = new Plate(width, length);
|
|
plate.Parts.Add(new Part(drawing, new Vector(3, 4)));
|
|
nest.Plates.Add(plate);
|
|
}
|
|
return nest;
|
|
}
|
|
|
|
private static string NewTempDir()
|
|
{
|
|
var dir = Path.Combine(Path.GetTempPath(), $"cifiber-{Guid.NewGuid():N}");
|
|
Directory.CreateDirectory(dir);
|
|
return dir;
|
|
}
|
|
|
|
[Fact]
|
|
public void Post_OneProgramPerSheet_WritesNumberedFiles_EachAFullProgram()
|
|
{
|
|
var nest = MakeMultiSheetNest((48, 96));
|
|
nest.Plates.Add(new Plate(60, 120)); // empty sheet: not posted
|
|
var dir = NewTempDir();
|
|
try
|
|
{
|
|
var chosen = Path.Combine(dir, "JOB.cnc");
|
|
var post = new CIFiberPostProcessor(MakeConfig());
|
|
|
|
var expected = post.GetOutputFiles(nest, chosen);
|
|
post.Post(nest, chosen);
|
|
|
|
Assert.Equal(
|
|
new[] { Path.Combine(dir, "JOB-1.cnc"), Path.Combine(dir, "JOB-2.cnc") },
|
|
expected
|
|
);
|
|
Assert.Equal(
|
|
expected.OrderBy(f => f),
|
|
Directory.GetFiles(dir).OrderBy(f => f)
|
|
);
|
|
|
|
var first = Lines(File.ReadAllText(expected[0]));
|
|
var second = Lines(File.ReadAllText(expected[1]));
|
|
|
|
Assert.Contains("V.E.X_SIZE = 120.000", first);
|
|
Assert.Contains("V.E.X_SIZE = 96.000", second);
|
|
Assert.Contains("V.E.Y_SIZE = 48.000", second);
|
|
Assert.Contains("( Sheet number - 1 )", first);
|
|
Assert.Contains("( Sheet number - 2 )", second);
|
|
|
|
foreach (var program in new[] { first, second })
|
|
{
|
|
// Restart labels start over in every program.
|
|
Assert.Contains("N1:", program);
|
|
Assert.DoesNotContain("N3:", program);
|
|
Assert.Single(program, "M50");
|
|
Assert.Equal(
|
|
new[] { "L PROGRAMEND.NC", "M50", "M30", "%", "" },
|
|
program.TakeLast(5)
|
|
);
|
|
}
|
|
Assert.Contains("G0X3Y3.5", second); // hole pierce at the sheet-2 part location
|
|
}
|
|
finally
|
|
{
|
|
Directory.Delete(dir, true);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Post_SingleProgram_ChangesPalletBetweenSheets()
|
|
{
|
|
var nest = MakeMultiSheetNest((60, 120), (60, 120));
|
|
var config = MakeConfig();
|
|
config.OneProgramPerSheet = false;
|
|
var dir = NewTempDir();
|
|
try
|
|
{
|
|
var chosen = Path.Combine(dir, "JOB.cnc");
|
|
var post = new CIFiberPostProcessor(config);
|
|
|
|
Assert.Equal(new[] { chosen }, post.GetOutputFiles(nest, chosen));
|
|
post.Post(nest, chosen);
|
|
Assert.Equal(new[] { chosen }, Directory.GetFiles(dir));
|
|
|
|
var lines = Lines(File.ReadAllText(chosen));
|
|
Assert.Equal(3, lines.Count(l => l == "M50"));
|
|
Assert.Single(lines, "M30");
|
|
|
|
var sheet2 = lines.IndexOf("( Sheet number - 2 )");
|
|
Assert.Equal(
|
|
new[] { "( PART END )", "/L \"L0\"", "M50", "( Sheet number - 2 )" },
|
|
lines.Skip(sheet2 - 3).Take(4)
|
|
);
|
|
Assert.True(lines.IndexOf("( Sheet number - 3 )") > sheet2);
|
|
|
|
// Restart labels continue across sheets within one program.
|
|
Assert.Contains("N6:", lines);
|
|
Assert.Contains("V.E.R3=6", lines);
|
|
}
|
|
finally
|
|
{
|
|
Directory.Delete(dir, true);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Post_SingleProgram_RejectsMixedSheetSizes_WritingNothing()
|
|
{
|
|
var nest = MakeMultiSheetNest((48, 96));
|
|
var config = MakeConfig();
|
|
config.OneProgramPerSheet = false;
|
|
var dir = NewTempDir();
|
|
try
|
|
{
|
|
var ex = Assert.Throws<InvalidOperationException>(() =>
|
|
new CIFiberPostProcessor(config).Post(nest, Path.Combine(dir, "JOB.cnc"))
|
|
);
|
|
Assert.Contains("One program per sheet", ex.Message);
|
|
Assert.Empty(Directory.GetFiles(dir));
|
|
}
|
|
finally
|
|
{
|
|
Directory.Delete(dir, true);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Post_OneProgramPerSheet_OversizeSheet_WritesNoFiles()
|
|
{
|
|
var nest = MakeMultiSheetNest((60, 200)); // second sheet 200 long > 160.25 table
|
|
var dir = NewTempDir();
|
|
try
|
|
{
|
|
var ex = Assert.Throws<InvalidOperationException>(() =>
|
|
new CIFiberPostProcessor(MakeConfig()).Post(nest, Path.Combine(dir, "JOB.cnc"))
|
|
);
|
|
Assert.Contains("exceeds maximum table X", ex.Message);
|
|
Assert.Empty(Directory.GetFiles(dir));
|
|
}
|
|
finally
|
|
{
|
|
Directory.Delete(dir, true);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Post_OneProgramPerSheet_SingleSheet_UsesChosenName()
|
|
{
|
|
var nest = MakeSquareWithHoleNest();
|
|
var dir = NewTempDir();
|
|
try
|
|
{
|
|
var chosen = Path.Combine(dir, "JOB.cnc");
|
|
var post = new CIFiberPostProcessor(MakeConfig());
|
|
|
|
Assert.Equal(new[] { chosen }, post.GetOutputFiles(nest, chosen));
|
|
post.Post(nest, chosen);
|
|
|
|
Assert.Equal(Post(nest), File.ReadAllText(chosen));
|
|
}
|
|
finally
|
|
{
|
|
Directory.Delete(dir, true);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Post_PalletChangeCode_IsConfigurable()
|
|
{
|
|
var custom = MakeConfig();
|
|
custom.PalletChangeCode = " M51 ";
|
|
var lines = Lines(Post(MakeSquareWithHoleNest(), custom));
|
|
Assert.Equal(new[] { "L PROGRAMEND.NC", "M51", "M30", "%", "" }, lines.TakeLast(5));
|
|
Assert.DoesNotContain("M50", lines);
|
|
|
|
var none = MakeConfig();
|
|
none.PalletChangeCode = "";
|
|
lines = Lines(Post(MakeSquareWithHoleNest(), none));
|
|
Assert.Equal(new[] { "L PROGRAMEND.NC", "M30", "%", "" }, lines.TakeLast(4));
|
|
}
|
|
|
|
[Fact]
|
|
public void PostToStream_MultiSheetPerSheetMode_Throws()
|
|
{
|
|
var nest = MakeMultiSheetNest((60, 120));
|
|
var ex = Assert.Throws<InvalidOperationException>(() => Post(nest));
|
|
Assert.Contains("file path", ex.Message);
|
|
}
|
|
|
|
[Fact]
|
|
public void SavedConfigWithoutSheetSettings_DefaultsToPerSheetAndM50()
|
|
{
|
|
var config = System.Text.Json.JsonSerializer.Deserialize<CIFiberPostConfig>(
|
|
"""{ "MaxTableX": 120.0 }"""
|
|
)!;
|
|
|
|
Assert.True(config.OneProgramPerSheet);
|
|
Assert.Equal("M50", config.PalletChangeCode);
|
|
}
|
|
}
|